I'm Ashley Foster, a React and Rails developer in Boston, MA
May 2018
React Native Image Uploads with ActiveStorage

I’ve alway been interested in writing a mobile app and since I’ve had some free time recently, I decided to put my skills to the test and dive right into building a React Native app. I thought it would be fun and challenging to build an Instagram-like app while using Ruby on Rails for my API.

After setting up Redux and user authentication, it’s finally time to create the posts. A post should have some text as well as an Image. After contemplating what my best course of action would be for attaching an image, I was reminded about ActiveStorage(a new feature that was recently released for Rails in 5.2). So naturally, I updated my Rails app to 5.2 and started setting up ActiveStorage!

After the initial setup, I realized there wasn’t much documentation on how to use ActiveStorage with a JSON API. With a lot of research, trial, and error, I finally figured out how to get ActiveStorage working in my JSON endpoint.

The first step is to build a post form to select the image. To do this, we use react-native-image-picker.

class Form extends React.Component {
  state = {
    imageData: null,
  }

  getPhotoFromGallery = () => {
    ImagePicker.launchImageLibrary(null, (response)  => {
      console.log('Response = ', response);

      if (response.didCancel) {
        console.log('User cancelled image picker');
      }
      else if (response.error) {
        console.log('ImagePicker Error: ', response.error);
      }
      else {
        this.setState({ imageData: response });
      }
    });
  };

  onSubmit = () => {
    const { imageData } = this.state
    const { onSubmit } = this.props

    onSubmit({ imageData })
  }

  showPickedImage() {
    const { imageData } = this.state;

    if (imageData !== null) {
        return (
          <Image
          source={{ uri: imageData.uri }}
          style={{ alignSelf: 'center', width: 200, height: 200 }}
          />
        );
    } else {
      return (
        <View>
          <TouchableHighlight
            style={styles.addPhoto}
            onPress={this.getPhotoFromGallery}
          >
            <Text style={styles.addPhotoText}>Add Photo</Text>
          </TouchableHighlight>
        </View>
      );
    }
  }

  render() {
    const { image, onSubmit } = this.props;

    return (
      <ScrollView>
        <View style={styles.container}>
          {this.showPickedImage()}

          <Button style={styles.submit}
            onPress={this.onSubmit}
          >
            Add Post
          </Button>
        </View>
      </ScrollView>
    );
  }
}

export default Form;

We can then pass the form data up to the Redux createPost action. react_native_image_picker returns an object that contains a data property, which represents the image in base64. This is exactly what we need in order to upload the image to the API. In addition we also include the image’s filename.

export const createPost = (title, description, imageData) => {
  return (dispatch, getState) => {
    const currentUser = getState().currentUser

    dispatch({
      type: 'LOAD_SPINNER'
    });

    return fetch(`${URL}posts`, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'X-User-Email': currentUser.email,
        'X-User-Token': currentUser.authentication_token,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        post: {
          title,
          description,
          image: imageData.data,
          file_name: imageData.fileName,
        }
      })
    }).then((response) => {
      if (response.ok) {
        response.json().then((data) => {
          dispatch({
            type: 'POST_CREATE_SUCCESS',
            payload: data
          });
        });
      } else {
        dispatch({
          type: 'POST_CREATE_FAILED'
        })

        return Promise.reject('error')
      };
    }).catch(e => {
      return Promise.reject(e)
    })
  };
};

Now that the mobile app is sending the image in the correct format, we can focus on the API.

class Api::PostsController < Api::ApiController
  respond_to :json

  def create
    @post = Post.new(post_params)
    @post.user = current_user
    @post.image.attach(io: image_io, filename: image_name)

    unless @post.save
      puts @post.errors.inspect
      render json: { error: "Unable to create post" }, status: 422
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :description)
  end

  def image_io
    decoded_image = Base64.decode64(params[:post][:image])
     StringIO.new(decoded_image)
  end

  def image_name
    params[:post][:file_name]
  end
end

We add in two private methods image_name(which grabs the file_name) and image_io(which decodes the attached image’s data into a StringIO). Next, we use ActiveStorage’s attach (ActiveStorage section 3.3) method which allows us to pass it an IO stream (which is why we decode the base64 into a StringIO object) as well as a file name.

The next step is to return a link to the image in the JSON payload we send to the mobile app.

json.(
  post,
  :id,
  :created_at,
  :title,
  :description,
  :user,
)
json.image_url url_for(post.image)

With the image uploading and returning the url for the image, all that’s left is to render it in the app.

class Card extends React.Component {
  state = {
    loading: true,
  }

  handleLoad = () => {
    this.setState({ loading: false });
  }

  render() {
    const { image } = this.props;
    const { loading } = this.state;

    return (
      <View style={styles.container}>
        <View style={styles.image}>
          {loading && (
            <ActivityIndicator style={StyleSheet.absoluteFill} size={'large'} />
          )}

          <Image
            style={StyleSheet.absoluteFill}
            source={{uri: `${URL}${image}`}}
            onLoad={this.handleLoad}
          />
        </View>
      </View>
    );
  }
}

export default Card;

Woohoo! With these changes we are now able to attach an image from the camera roll, store the image, and render the image on the client side.

I hope this helps some fellow developers wanting to try out this awesome new addition to Rails. Happy coding!