I have an array of texts that I want to flash on a blank screen, one after the other with animations. Something like:
state = {
meditations: ["Take a deep breath", "embrace this feeling", "breath
deeply", ...]
}
I want to show only one string at a time, and animate their opacity. So a string fades in and fades out, then the next string, and so on.
I am new to react native and quite confused about how to go about this. Please, how may I approach this, I have read the docs but still not clear how to.
Below is what I have tried, I modified this from the docs but it shows everything at once. I'm still trying to see how I can make them animate one after the other, showing only one at a time. Thanks for your help in advance.
import React from 'react';
import { Animated, Text, View } from 'react-native';
class FadeInView extends React.Component {
state = {
fadeAnim: new Animated.Value(0), // Initial value for opacity: 0
}
renderMeditations() {
let { fadeAnim } = this.state;
return this.props.meditations.map((meditation, index) => {
Animated.timing( // Animate over time
this.state.fadeAnim, // The animated value to drive
{
toValue: 2, // Animate to opacity: 1 (opaque)
duration: 10000, // Make it take a while
}
).start(() => {
this.setState({ fadeAnim: new Animated.Value(0) })
}); // Starts the animation
return (
<Animated.Text // Special animatable View
key={index}
style={{
...this.props.style,
opacity: fadeAnim, // Bind opacity to animated value
}}
>
{meditation}
</Animated.Text>
)
})
}
render() {
return (
<View style={{flex: 1}}>
{this.renderMeditations()}
</View>
);
}
}
export default class App extends React.Component {
state = {
meditations: ["Take a deep breath", "Calm down", "Relax", "Tell yourself all will be fine"]
}
render() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<FadeInView meditations={this.state.meditations} style={{fontSize: 28, textAlign: 'center', margin: 10}} />
</View>
)
}
}
After much toil with this, I was able to solve it with react-native-animatable like so:
import React from "react";
import {
View,
Text,
Animated
} from "react-native";
import * as Animatable from 'react-native-animatable';
class VideoScreen extends React.Component {
state = {
meditations: ["Take a deep breath", "embrace this feeling", "breath
deeply"],
index: 0
};
render() {
const { meditations, index } = this.state;
return (
<View style={{flex: 1}}>
<Animatable.Text
key={index}
animation={'fadeIn'}
iterationCount={2}
direction="alternate"
duration={2000}
onAnimationEnd={() => {
if (this.state.index < this.state.meditations.length - 1) {
this.setState({ index: this.state.index + 1});
}
}}
style={{
position: "absolute",
left: 0, right: 0,
bottom: 40
}}>
{meditations[index]}
</Animatable.Text>
</View>
);
}
}
export default VideoScreen;
The map function executes all at once so basically you are rendering/returning all 3 items at the same time. I understand that your issue is that the animation is working tho.
If what you want is to show one text, then the other and so on I suggest iterating the index of your text array instead of using the map function.
Something like:
Execute Animation
Increase Index
Index = 0 if you are at the end of the array.
In a loop. Check setInterval, it might help you.
For the function components:-
we can use the above-metioned solutions. I am writing a function hopefully it will help you display a looping text with the animation
We will use this package for the animation https://github.com/oblador/react-native-animatable.
import {StyleSheet} from 'react-native';
import React, {useState} from 'react';
import * as Animatable from 'react-native-animatable';
const UserMessage = () => {
const [index, setIndex] = useState(0);
const meditations = [
'Take a deep breath',
'embrace this feeling',
'breath deeply',
];
return (
<Animatable.Text
key={index}
animation={'fadeIn'}
iterationCount={2}
direction="alternate"
duration={2000}
onAnimationEnd={() => {
if (index < meditations.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
}}
style={styles.messageStyle}>
{meditations[index]}
</Animatable.Text>
);
};
export default UserMessage;
const styles = StyleSheet.create({
messageStyle: {
textAlign: 'center',
fontSize: 18,
fontWeight: '500',
width: '80%',
color: '#1C1C1C',
marginBottom: 20,
minHeight: 50,
alignSelf: 'center',
},
});
Related
I am new to React Native. I am trying to build an app which has a Splash screen that would later navigate to Login screen if a user has not been authenticated or the Main screen if the user is authenticated. This is done using this.props.navigation.navigate()
The problem is that the Splash component would be mounted twice. I checked this by printing inside componentDidMount() of Splash. Because of this, the Login/Main screen enters twice, which looks very unpleasant. Is there any way to fix this?
Also, I want to add some delay when the screen changes from Splash to Login or Main using setTimeout(). Anyway to go about doing this?
Here's my code:
index.js
import React from 'react';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
import reduxThunk from 'redux-thunk';
import reducers from './src/reducers';
import { StyleSheet } from 'react-native';
import LoginScreen from './src/components/Login/LoginScreen';
import Splash from './src/components/Login/Splash';
import Navigation from './src/components/Navigation/Navigation';
import { Font } from 'expo';
import {
createStackNavigator
} from 'react-navigation';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);
const persistor = persistStore(store);
export default class App extends React.Component {
constructor(props){
super(props);
this.state = {
fontLoaded: false,
currentScreen: 'Splash',
};
setTimeout(() => this.setState({currentScreen: 'Login'}), 2000);
}
async componentDidMount() {
await Font.loadAsync({
'Quicksand': require('./assets/fonts/Quicksand-Regular.ttf'),
'Quicksand-Medium': require('./assets/fonts/Quicksand-Medium.ttf'),
'Quicksand-Bold': require('./assets/fonts/Quicksand-Bold.ttf'),
});
this.setState({ fontLoaded: true });
}
render() {
const MainNavigator = createStackNavigator({
Splash: { screen: Splash },
Main: { screen: Navigation },
Login: { screen: LoginScreen },
})
if (this.state.fontLoaded)
return (
<Provider store={store}>
<MainNavigator></MainNavigator>
</Provider>
)
else return null;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Splash.js
import React from 'react';
import { StyleSheet, Text, View, ImageBackground, Image, Button } from 'react-native';
import bgImage from '../../../assets/images/login-background2.png';
import logo from '../../../assets/images/app-logo.png';
import { connect } from 'react-redux';
import { checkAuth } from '../../actions/auth.actions';
class Splash extends React.Component {
static navigationOptions ={
header: null
}
constructor(props){
super(props);
this.state = {
stillLoading: true,
}
}
componentDidMount() {
this.props.checkAuth();
}
render() {
if (this.props.authState.isLoginPending)
return (
<ImageBackground source={bgImage} style={styles.backgroundContainer}>
<View style={styles.logoContainer}>
<Image source={logo} style={styles.logo}></Image>
<Text style={styles.logoText}> Welcome to HealthScout</Text>
</View>
</ImageBackground>
);
else if (this.props.authState.isLoginSuccess){
setTimeout(() => this.props.navigation.navigate('Main'));
return null;
}
else{
setTimeout(() => this.props.navigation.navigate('Login'));
return null;
}
}
}
const mapStateToProps = state => {
return {
authState: state.authState
}
}
const mapDispatchToProps = dispatch => {
return {
checkAuth: () => dispatch(checkAuth()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Splash);
const styles = StyleSheet.create({
backgroundContainer: {
flex: 1,
alignItems: 'center',
width: null,
height: null,
justifyContent: 'center',
},
logoContainer: {
alignItems: 'center',
},
logo: {
width: 110,
height: 149,
},
logoText: {
color: '#fff',
fontSize: 40,
fontFamily: 'Quicksand',
opacity: 0.7,
marginTop: 20,
marginBottom: 10,
textAlign: 'center',
},
});
Solution
Take out the createStackNavigator from render.
It is better way wrapping screens above App class.
const MainNavigator = createStackNavigator({
Splash: { screen: Splash },
Main: { screen: Navigation },
Login: { screen: LoginScreen },
})
export default class App extends React.Component {
...
Why?
render is run repeatedly depends on various conditions as changing state, props and so on.
And your code looks making multiple components with createStackNavigation in render. Take out :)
p.s If you want to wait loading fonts before show home screen, just change to home screen from splash screen after loaded fonts. Thus, the better way is loading fonts in SplashScreen and do what you want.
What is the best pattern, in react native, to animate components on state change?
For example I have a list of elements and tapping on one I want it to disappear and the ones below him to 'get up' filling the missing space
How can I make the transition smooth?
React-natives own animated API works really well.
Basically you have a value in state, which you connect with a style props, and change that value over time. (for examples follow link)
For smooth animations use usenativedriver (not always possible) and also, make sure you don't have debugger runnning in emulated/real device
EDIT: 2018-05-31
This is an example of how I've used it. Probably exist other ways of doing it
import { Animated, Text} from 'react-native';
class ShowCaseAnimation extends Component {
state = {
animations: {
height: new Animated.Value(0),
fade: new Animated.Value(0),
}
}
componentDidMount() {
const { height, fade } = this.state.animations;
if (this.props.animate) {
doneAnimation({ height, fade }).start(() => {
// Do stuff after animations
});
}
}
render() {
const { animations } = this.state;
return (
<Animated.View
style={{
height: animate? animations.height : 300,
opacity: animate? animations.fade: 1,
// other styling
}}
>
<Text> All your base are belong to us </Text>
</Animated.View>
);
}
}
*doneAnimation: *
import { Animated, Easing } from 'react-native';
export const doneAnimation = ({ height, fade }) => Animated.parallel([
Animated.timing(height, {
toValue: 300,
easing: Easing.elastic(),
duration: 500,
delay: 1500,
}),
Animated.timing(fade, {
toValue: 1,
easing: Easing.ease,
duration: 1000,
delay: 1500,
}),
]);
export default doneAnimation;
doneAnimation will change the state and perform the described animations.
This is how you can trigger an animation on state change in a functional component.
Say you have a Button that changes state with onPress:
<Button title="toggle" onPress={() => setValue(!Value)} />
then you can trigger the animation inside a useEffect with the Value
that changes in the dependency array:
const [Value, setValue] = useState(true);
useEffect(() => {
// Input your animation here
// ...
}, [Value]);
I'm trying to retrieve the current color from a react-native animation. It's mapped through interpolate to a set of color strings.
class IconTransition extends React.Component<Props, State> {
protected _param: number = 0;
constructor(props: Props) {
super(props);
this.state = {
param: new Animated.Value(0)
};
this.state.param.addListener(param => {
this._param = param.value;
});
}
componentDidMount() {
Animated.spring(this.state.param, {
mass: 1,
stiffness: 10,
damping: 10,
toValue: 1
});
}
componentWillReceiveProps() {
// I want to do something like this. Would be awesome
// if I could avoid the listener in the constructor.
//
// const currentColor = Animated.interpolate.get({
// currentInput: this._param,
// outputRange: ["#FFFFFF", "#000000"]
// });
}
render() {
return (
<AnimatedIcon
{...this.props}
color={this.state.param.interpolate({
inputRange: [0, 1],
outputRange: ["#FFFFFF", "#000000"]
})}
/>
);
}
}
I want to retrieve the color, as interpolated, should the animation not finish. I'm aware I could probably use an external library such a chroma-js (in particular, the chroma.mix function) to achieve this - but there are different ways to interpolate through two different colors and I'd rather not depend on an external library if I can avoid it.
So... the greater question remains, how can I imperatively request an output value from the interpolation API? Can we not listen on interpolated values, just as we do with Animated.Value()?
I was trying to do the same for a while now and there's a few things you need to keep in mind:
Since you're trying to update a prop that is not a style prop your
best bet is to use the addListener and setNativeProps methods
https://facebook.github.io/react-native/docs/animations#setnativeprops
The interpolation has a __getValue method to check the current value.
This is the function you want to call in the listener to check the
current interpolated value
https://github.com/facebook/react-native/blob/master/Libraries/Animated/src/nodes/AnimatedInterpolation.js#L327
Colors cannot be passed to props like that when set from setNative
props, it has to be passed through processColor
https://github.com/facebook/react-native/blob/master/Libraries/StyleSheet/processColor.js
If you put that all together you can get somthing like the following, which worked in my case:
import React from 'react';
import {View, processColor} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
class BackgroundColorLinearGradientText extends React.Component {
/**
* Class constructor.
*/
constructor(props, context) {
super(props, context);
this.backgroundColor = new Animated.Value(0);
this.backgroundColor.addListener(i => {
let interpolated = this.backgroundColor.interpolate({
inputRange: [0, 1],
outputRange: ['#FF0000', '#00FF00'],
}).__getValue();
if (this.background) {
this.background.setNativeProps({colors: [processColor(interpolated), processColor(this.background.props.colors[1])]})
}
});
}
componentDidMount() {
Animated.timing(this.backgroundColor, {
toValue: 1,
duration: 3000,
}).start();
}
render() {
return (
<LinearGradient ref={i => this.background = i} colors={['red', 'blue']} style={{flex: 1}}>
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
Content
</View>
</LinearGradient>
);
}
}
This will create a screen which has a red to blue gradient background, transitioning to green to blue in three seconds.
I'm new to react-native and need some help with Animate.
Goal: to animate Image, so that it looks like its slowly breathing.(getting a little larger then smaller, then back again, constantly, like someone breathing in and out)
My images are stored in an array, inside newOrder() method:
newOrder(timeAsProp) {
const hour = timeAsProp.slice(0, 2);
let returnValue = [];
const yud = <Image key="yud" source={require('./img/yud.png')} style={Style.image} />;
const hey1 = <Image key="hey1" source={require('./img/hey1.png')} style={Style.image} />;
const vav = <Image key="vav" source={require('./img/vav.png')} style={Style.image} />;
const hey2 = <Image key="hey2" source={require('./img/hey2.png')} style={Style.image} />;
return (
<View style={Style.displayContainer}>{returnValue}</View>
);
called in the render method, like this:
{this.newOrder(parsedTime)}
its four seperate images, which are rendered and displayed together on one line.
it looks like this:
letters being rendered to one word:
its important that the Image as a whole, should be breathing together in unison, and not each image on its own.
heres a screen pic so you see what the image looks like, if that will help you understand the best method to make it look alive:
edit:
something that would add to the animation i think, would be two things:
1)size getting larger and smaller
2)actual color patch on the letters slightly moving, maybe closer and further, like zooming in and out or something like that.
i think those two together would make the breathing 3d.
so im interested in hearing peoples opinions how to do this...
thnks!
Use a sequence of animations in a loop.
In this example I am breathing a text.
First change the opacity from 1 to 0, then, change the opacity back to 1.
You can use this principle to change other properties, like width and height.
import React, {Component} from 'react'
import {
Animated,
Easing
} from 'react-native'
export default class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
opacity: new Animated.Value(1)
}
}
componentDidMount() {
Animated.loop(
Animated.sequence([
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 1000,
ease: Easing.linear,
useNativeDriver: true
}),
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 1000,
ease: Easing.linear,
useNativeDriver: true
})
])
).start();
}
render() {
return(
<Animated.View style={{opacity: this.state.opacity}}>
<Text>I'm breathing</Text>
</Animated.View>
);
}
}
So for an infinite animation (that you can stop on your own), you can set the width and height of all of the images to the same interpolated Animated value. To generate a breathing effect, one possible way to do this is to tie two animation functions together with one increasing and the other decreasing. For example:
import React, { Component } from 'react';
import { View, StyleSheet, Animated, Image, Easing } from 'react-native';
import { Constants } from 'expo';
const AnimatedImage = Animated.createAnimatedComponent(Image);
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
size: new Animated.Value(1)
}
}
componentDidMount () {
this._loopAnimationUp();
}
// The animation functions. Initial and end values can be anything (not just 1, 10, but remember to use the same value and flip them:
_loopAnimationUp() {
this.state.size.setValue(1);
Animated.timing(this.state.size, {
toValue: 10,
duration: 5000,
easing: Easing.linear
}).start((o) => {
if (o.finished) {
this._loopAnimationDown();
}
});
}
_loopAnimationDown() {
this.state.size.setValue(10);
Animated.timing(this.state.size, {
toValue: 1,
duration: 5000,
easing: Easing.linear
}).start((o) => {
if (o.finished) {
this._loopAnimationUp();
}
});
}
render() {
const size = this.state.size.interpolate({
inputRange: [1, 10],
outputRange: [10, 50],
extrapolate: 'clamp',
});
return (
<View style={styles.container}>
<AnimatedImage
style={[styles.image, {
width: size,
height: size,
}]}
source={{uri: 'http://placekitten.com/g/200/200'}}
/>
<AnimatedImage
style={[styles.image, {
width: size,
height: size,
}]}
source={{uri: 'http://placekitten.com/g/200/200'}}
/>
<AnimatedImage
style={[styles.image, {
width: size,
height: size,
}]}
source={{uri: 'http://placekitten.com/g/200/200'}}
/>
<AnimatedImage
style={[styles.image, {
width: size,
height: size,
}]}
source={{uri: 'http://placekitten.com/g/200/200'}}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
flexDirection: 'row',
},
image: {
justifyContent:'center',
backgroundColor:'transparent'
},
});
If you need to later stop the animation, you can use:
this.state.size.stopAnimation();
You can see a working implementation of it here using placeholder images.
For the more math inclined, there is probably a way to accomplish this with a single looping animation and using interpolation in a more complex manner.
I'm having a problem with React Native and Parse JS SDK.
And I'm using ParseReact
I have built a login, sign up and a main view, Sign up and log in works well but after i logged in -> directed to main view and when I refresh the app (CMD+R) in my simulator, it brings me back to the login view again, i should be brought to Main view.
As you can see I have set a state for initialComponent:
this.state = {
InitialComponent : ((!currentUser) ? LoginView : MainView)
};
This allows my navigator to check for currentUser is null then load LoginView as initial component, else set Main View(user logged in)
'use strict';
var React = require('react-native');
var MainView = require('./MainView');
var LoginView = require('./LoginView');
var Parse = require('parse').Parse;
var ParseReact = require('parse-react');
Parse.initialize("mykey", "mykey");
var {
AppRegistry,
StyleSheet,
Text,
View,
TextInput,
TouchableHighlight,
Navigator,
Component
} = React;
class MyApp extends Component {
constructor(props) {
super(props);
var currentUser = Parse.User.current();
console.log('Current User:' + currentUser);
this.state = {
InitialComponent : ((!currentUser) ? LoginView : MainView)
};
}
render() {
return (
<Navigator
initialRoute={{
name : 'StatusList',
component: this.state.InitialComponent
}}
configureScene = {() =>{
return Navigator.SceneConfigs.FloatFromRight;
}}
renderScene={(route, navigator) =>{
if(route.component) {
return React.createElement(route.component, {navigator});
}
}}/>
);
}
}
AppRegistry.registerComponent('MyApp', function() { return MyApp });
In my Xcode console, i kept getting current user is null after each refresh even though i have previously logged in. On my parse app, I can see new session has been created.
In my LoginView.
'use strict';
var React = require('react-native');
var SignUp = require('./SignUp');
var MainView = require('./MainView');
var {
AppRegistry,
StyleSheet,
Text,
View,
TextInput,
TouchableHighlight,
Navigator,
AlertIOS,
Component
} = React;
var styles = StyleSheet.create({
container : {
flex: 1,
padding: 15,
marginTop: 30,
backgroundColor: '#0179D5',
},
text: {
color: '#000000',
fontSize: 30,
margin: 100
},
headingText: {
color: '#fff',
fontSize: 40,
fontWeight: '100',
alignSelf: 'center',
marginBottom: 20,
letterSpacing: 3
},
textBox: {
color: 'white',
backgroundColor: '#4BB0FC',
borderRadius: 5,
borderColor: 'transparent',
padding:10,
height:40,
borderWidth: 1,
marginBottom: 15,
},
greenBtn: {
height: 36,
padding: 10,
borderRadius: 5,
backgroundColor: '#2EA927',
justifyContent: 'center'
},
signUpButton: {
marginTop: 10,
height: 36,
padding: 10,
borderRadius: 5,
backgroundColor: '#FF5500',
justifyContent: 'center'
},
btnText: {
color : '#fff',
fontSize: 15,
alignSelf: 'center'
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
loginForm : {
flex:1,
marginTop:100
}
});
class LoginView extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
checkLogin() {
var success = true;
var state = this.state;
for(var key in state){
if(state[key].length <= 0){
success = false;
}
}
if(success) {
this._doLogin();
} else {
//show alert
AlertIOS.alert('Error','Please complete all fields',
[{text: 'Okay', onPress: () => console.log('')}]
);
}
}
goMainView() {
this.props.navigator.push({
title: "Home",
component: MainView
});
}
goSignUp() {
this.props.navigator.push({
title: "Sign Up",
component: SignUp
});
}
_doLogin() {
var parent = this;
Parse.User.logIn(this.state.username, this.state.password, {
success: function(user) {
parent.goMainView();
},
error: function(user, error) {
AlertIOS.alert('Login Error', error.message,
[{text: 'Okay', onPress: () => console.log('')}]
);
}
});
}
onUsernameChanged(event) {
this.setState({ username : event.nativeEvent.text });
}
onPasswordChanged(event) {
this.setState({ password : event.nativeEvent.text });
}
render() {
return(
<View style={styles.container}>
<View style={styles.loginForm}>
<Text style={styles.headingText}>
MyStatus
</Text>
<TextInput style={styles.textBox}
placeholder='Username'
onChange={this.onUsernameChanged.bind(this)}
placeholderTextColor='#fff'
autoCorrect={false}
>
</TextInput>
<TextInput style={styles.textBox}
placeholder='Password'
onChange={this.onPasswordChanged.bind(this)}
placeholderTextColor='#fff'
password={true}
>
</TextInput>
<TouchableHighlight style={styles.greenBtn}
underlayColor='#33B02C'
onPress={this.checkLogin.bind(this)}>
<Text style={styles.btnText}>Login</Text>
</TouchableHighlight>
<TouchableHighlight style={styles.signUpButton}
underlayColor='#D54700'
onPress={this.goSignUp.bind(this)}>
<Text style={styles.btnText}>Sign Up</Text>
</TouchableHighlight>
</View>
</View>
);
}
}
module.exports = LoginView;
Am I doing it the wrong way? Kindly advice. Or is it something wrong with parse localstorage/session?
Because React Native only provides asynchronous storage, we're forced to make Parse.User.current() an asynchronous call that returns a Promise. Since you're already using Parse+React, it's really easy to handle this. Your component that changes based upon whether the user is logged in or not (MyApp) should subscribe to ParseReact.currentUser. This stays synchronized with the current user, and allows your component to automatically update the moment the user logs in or out. For a demo of this, look at the AnyBudget demo in the GitHub repo:
In react-native...
import Parse from 'parse/react-native';
Parse.User.currentAsync()
.then((currentUser)=>{
if (currentUser) {
Alert.alert('', JSON.stringify(currentUser))
}
});
At first in my case I use ParseReact.Mutation that work perfectly.
After that I got a requirement that some field need to be unique, so I use Parse.Cloud to solve problem.
// For normally situaltion is work fine.
ParseReact.Mutation.Set(this.data.myObject, {
normal_field: this.state.value
}).dispatch();
// But if I use Parse.Cloud. Is there any solution to sync data to ParseReact.currentUser?
Parse.Cloud.run('setUniqueField', {
userID: this.data.user.id.objectId,
uniqueField: this.state.value,
}, {
success: function (response) {
// It doesn't work!
// ParseReact.currentUser.update();
this.props.navigator.pop();
}.bind(this),
error: function (error) {
console.log('[Parse Cloud Error]: ', error);
}
});
#Andrew's solution (alone) didn't work for me. In case anyone else is stuck, here's what was happening: subscribing to Parse.User.current() didn't work, and this.data.user was always null.
To fix this, I had to change:
var Parse = require('parse').Parse;
var ParseReact = require('parse-react');
to
var Parse = require('parse/react-native').Parse;
var ParseReact = require('parse-react/react-native');
then everything started working beautifully. It was loading the browser version of the parse modules until I forced it to load the react-native versions.