見出し画像

アプリがすぐに作れる!React Native(expo) + Rails API(devise token auth)のサンプルコード

更新内容:8/2
下記2点の機能を追加しました!
・画像アップロード機能
・ドラッグダウンで最新の情報を取得する機能


※注意点です
Railsは動作検証できておりますが、React NativeはiOSのみ検証が済んでいます。Androidの確認はもう少し後になるので、Androidの検証も済んでからが良い、という方は購入をしばらくお待ちください。

Rails(devise token auth)とReact Nativeの記事が少なく結構苦労したので、
連結部分とCRUDのやりとり部分を作成して他のプロジェクトに使えるようにしてみました。

まだドキュメントが整理されていないので、ソースコード料として2,000円で販売していますが、ドキュメントが出来上がったら4,000円ぐらいで販売する予定です。

React以外のAngularやVueを扱っている人であればソースコードは問題なく読めると思うので、安いうちに購入をお勧めします!

概要

アプリの概要動画↑

イベントアプリ情報を掲載できる掲示板のようなシンプルなアプリで、
devise token authをベースとした認証機能とCRUD対応しています

このサンプルコードを使うことで、
・CRUD
・APIとの連携方法の調査
など土台に利用することができます

実際に私はこのベースを使って別のプロジェクトに応用しているので
これをそのまま使うのではなく、reduxや認証機能など
使いまわせるところは使い回す、というのがお勧めです!

ソースコードの簡単な説明

こちら一部のファイルを抜粋しています。
特にclientはどのように実装されているか不安だと思いますので
いくつかご提示しています。
また、RailsもGemfileを公開しています。

clientのファイル構成
・components→再利用する可能性がある機能
・redux→redux関連の機能
・screens→それぞれ画面を作成する機能
・shared→apiのリンクやCRUDのメソッドをまとめている
・router.js→ルーティング設定

個人的にはこのアーキテクチャーが一番価値があるのではないか?と思ってます

画像1

こちらは具体的なコードのサンプルです
classかfunctionかでかなり悩みましたが、
最近はclassではなくfunctionで実装すること流れになっているようなので、
functionで実装を進めました。
classからfunctionへ流れている理由はコードが冗長になってしまうことが原因のようです。

ちなみにコードですが、
できる限り同じスタイルで実装することを心掛けました
なので、一つ理解できれば、全てのファイルを読むことができるようになるかと思います

import React, { useState } from 'react';
import { ActivityIndicator, StyleSheet, Button as ReactNativeButton } from 'react-native';
import { Container, Header, Button, Text, Content, Form, Item, Input, Label } from 'native-base';
import { setAuthData, login } from '../shared/auth_service';
import { useDispatch } from 'react-redux';
import { addCurrentUser } from '../redux/actions/current_user';
import { Ionicons, MaterialIcons } from '@expo/vector-icons';


export default function LoginScreen(props) {

   const dispatch = useDispatch();

   // 必要な変数を定義
   const [isLoading, setIsLoading] = useState(false);
   const [isFailed, setIsFailed] = useState(false);
   const [email, setEmail] = useState('');
   const [password, setPassword] = useState('');

   const onSubmit = () => {
       setIsLoading(true);
       return (
           login(email, password)
               .then(response => {
                   setIsLoading(false);
                   const token = response.headers['access-token'];
                   const client = response.headers['client'];
                   const uid = response.headers['uid'];
                   if (token && client && uid) {
                       // userのログイン情報をreduxで管理
                       dispatch(addCurrentUser(response.data.data))
                       // storageにtoken情報などを追加
                       setAuthData(token, client, uid);
                       setIsFailed(false);
                       props.navigation.navigate('Home');
                   } else {
                       setIsFailed(true);
                   }
               }).catch(data => {
               setIsLoading(false);
           })
       );
   }

   const loginButton = () => {
       if (isLoading) {
           return <ActivityIndicator size='small'/>;
       } else {
           return (
               <Button style={ styles.button } onPress={ () => onSubmit() }>
                   <Text style={ styles.text }>ログイン</Text>
               </Button>
           )
       }
   }

   return (
       <Container>
           <Header/>
           <Content style={ styles.main }>
               { isFailed && <Text>ログインに失敗しました。</Text> }
               <Form>
                   <Item inlineLabel>
                       <MaterialIcons name='email' style={ styles.icon } size={ 24 } color='black'/>
                       <Input placeholder='Email' onChangeText={ (email) => setEmail(email) }/>
                   </Item>
                   <Item inlineLabel last>
                       <Ionicons name='md-key' style={ styles.icon } size={24} color='black' />
                       <Input
                           placeholder='パスワード'
                           secureTextEntry={ true }
                           onChangeText={ (password) => setPassword(password) }/>
                   </Item>
               </Form>
               { loginButton() }
               <ReactNativeButton title='会員登録する' onPress={ () => props.navigation.navigate('Signup') }/>
           </Content>
       </Container>
   );
}

const styles = StyleSheet.create({
   icon: {
       marginRight: 10
   },
   main: {
       height: '90%',
       backgroundColor: '#eee',
   },
   textInput: {
       height: 60,
       width: 300,
       paddingLeft: 20,
       margin: 10,
       borderWidth: 1,
       borderRadius: 8,
   },
   button: {
       margin: 30
   },
   text: {
       marginLeft: 'auto',
       marginRight: 'auto',
       marginTop: 'auto',
       marginBottom: 'auto',

   }
});

ちなみにこちらはAPIとの連結部分
Promiseオブジェクト作成しているので、screensで読んでthenを使ったりできます


export async function findAll() {
   const config = await getAuthorization();
   return await axios.get(`${ apiPath }events`, config);
}

export async function save(title, body, address) {
   const config = await getAuthorization();
   const event = { title: title, body: body, address: address };
   return await axios.post(`${ apiPath }events/`, { event }, config);
}

次はAPIサーバーです
非常にシンプルなJSON APIです。こちらは特に触るところはなく、機能を追加する際の参考にしてください

module Api
 class EventsController < ApplicationController
   before_action :authenticate_api_user!
   before_action :set_event, only: %i[show update destroy]

   def index
     begin
       response_success(Event.all.map{ |event| event.to_dict })
     end
   rescue StandardError => e
     logger.error e.message
     response_bad_request
   end

   def show
     begin
       response_success(@event.to_dict)
     end
   rescue StandardError => e
     logger.error e.message
     response_bad_request
   end

・・・・・

Gemfile
devise token authをベースとしてAPIサーバーを作るためのgemを入れています

rspecも記述しているので、APIサーバーのテストの書き方の参考にしてください

# frozen_string_literal: true

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

gem 'rails', '~> 6.0.3', '>= 6.0.3.1'
gem 'puma', '~> 4.1'
gem 'bootsnap', '>= 1.4.2', require: false
gem 'jbuilder', '~> 2.7'

group :development, :test do
 gem 'dotenv-rails'
 gem 'rspec-rails', '~> 3.6.0'
 gem 'factory_bot_rails', '~> 4.10.0'
 gem 'ffaker'
 gem 'pry-rails'
 gem 'pry-remote'
 gem 'brakeman', '~> 4.7', require: false
 gem 'rails_best_practices', '~> 1.19', require: false
 gem 'rubocop', '~> 0.76', require: false
 gem 'rubocop-rails', '~> 2.3', require: false

 gem 'sqlite3', '~> 1.4'
 gem 'byebug', platforms: %i[mri mingw x64_mingw]
end

group :development do
 gem 'letter_opener'
 gem 'letter_opener_web'
 gem 'better_errors'
 gem 'rails-erd'
 gem 'listen', '~> 3.2'
 gem 'spring'
 gem 'spring-watcher-listen', '~> 2.0.0'
end

group :production do
 gem 'pg'
end

gem 'devise'
gem 'devise_token_auth'
gem 'rack-cors'
gem 'jp_prefecture' # イベントページに使う予定ですが、まだ使ってません
gem 'enumerize' # こちらもまだ使っていません


# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

起動方法

React Native:

$ npm install
$ npm start

でexpoの起動画面が開くので、iOSで実行します

画像2

実行するとエミュレーターが起動します
※起動時はコンパイルに時間がかかる可能性が高いです

画像3

Rails:

$ bundle install
$ yarn install
$ rails db:migrate
$ rails s -p 3001

Railsは特に設定などないためこれで問題ないかと思います。
※開発で利用したsqliteもソースコードに入っています

ソースコード

githubに招待することも考えましたが、購入後に招待だと遅いので
noteにzip形式で掲載することにしました

ここから先は

166字 / 2ファイル

¥ 2,000

この記事が気に入ったらサポートをしてみませんか?