React hooksで非同期通信のあるグローバルストア管理
ちょっとしっくりくるタイトルじゃない気がする。語彙力。
やりたいこと
例えば、ログインのよくある流れを考えてみる。
ストアの動きとして
1. ローディング状態をtrueにしてローダーを表示(dispatch)
2. ログイン通信
3. 取得したユーザデータをストアに保存(dispatch)
という動きが考えられる。
Reduxではredux-thunkなどを使ってaction内からdispatchすることで実現できたが、
Reduxを使わずにReact Hooksで実装する場合どうしたら良いか分からなかったので調べながらやってみた。
ストアの作成が趣旨なので、APIでのログイン処理の中身には触れません。
結論
ContextAPI + カスタムフック で対応できる
作成例
今回は一つですが、情報に応じてファイル分割も簡単にできそう。
ディレクトリは一旦全て同じ階層にあるという想定で。
ストア(reducer)の作成
まずは普通にreducerとactionを作る。
[actions.js]
export const AUTH_START = 'AUTH_START'; export const AUTH_SUCCESS = 'AUTH_SUCCESS'; export const AUTH_FAIL = 'AUTH_FAIL'; export const authStart = () => { return { type: AUTH_START, } } // 引数はログインの仕様に合わせて export const authSuccess = (id, email) => { return { type: AUTH_SUCCESS, id, email, } } export const authFail = (error) => { return { type: AUTH_FAIL, error } }
[reducer.js]
import { AUTH_START, AUTH_SUCCESS, AUTH_FAIL } from './actions'; export const initialState = { id: '', email: '', error: null, loading: false, } const reducer = (state = initialState, action) => { switch (action.type) { case AUTH_START: return { ...state, loading: true }; case AUTH_SUCCESS: return { ...state, id: action.id, email: action.email, error: null, loading: false }; case AUTH_FAIL: return { ...state, error: action.error, loading: false }; default: return state } } export default reducer;
普通のreducer。
Contextの作成
[context.js]
import React, {useReducer} from "react"; import reducer, { initialState } from "./reducer"; export const Context = React.createContext({}); export const Provider = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); return <Context.Provider value={{state, dispatch}}>{children}</Context.Provider>; };
[App.js]
import { Provider } from "./store/auth/context"; const App = () => { return ( <Provider> <ChildComponents /> </Provider> ); };
ChildComponents以下でuseContext(Context)することで、
Providerのvalueに渡した値がchildrenの中で使えるようになる。
グローバルストアは一応ここまでで完成。
カスタムフックを作成
今回やりたいことは、非同期通信の結果を待ってdispatchすることので、このままだとできない。
そのため、カスタムフックを作成する。
use~で分かりやすい名前を付ける。
[useAuth.js]
impor { useCallback } from "react"; import { authStart, authSuccess, authFail } from "./actions"; import axios from "axios"; export const useAuth = () => { const {state, dispatch} = useContext(Context); const login = useCallback((email, password) => { // ローディングを走らせる dispatch(authStart()); // ログイン通信 axios.post('/api/login', { email, password, }).then(res => { console.log(res); // 成功したらログイン情報をストアに保存 dispatch(authSuccess(res.data.user.id, res.data.user.email)); }).catch(err => { // 失敗したらエラーをストアに保存 console.log(err.response); dispatch(authFail(err)); }) }, [dispatch, authStart, authSuccess, authFail]); // ここでreturnしたものを各コンポーネントで使う return {state, login} };
カスタムフック内でContextを呼び出し、
loginメソッド内で複数回dispatchしています。
各コンポーネントではこのフックを介してストアにアクセスします。
使ってみる
[Login.js]
import React, { FormEvent, useState } from "react"; import { useAuth } from "./useAuth"; const Login = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const { state, login } = useAuth(); const loginHandler = (e) => { e.preventDefault(); login(email, password); }; let loader = null; if (state.loading) loader = <div>loading...</div>; return ( <React.Fragment> {loader} <div> <h2>ユーザー情報</h2> <p>id: {state.id}</p> <p>email: {state.email}</p> </div> <form onSubmit={loginHandler}> <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">Login</button> </form> </React.Fragment> ); }; export default Login;
ログイン処理中に「loading...」が表示され、
ログイン後にユーザー情報が表示されれば成功。
まとめ
カスタムフックのおかげで記述少なめでグローバルストアが定義できた。
Reduxの導入はいろいろ手間がかかるので、小さい規模なら十分というか、むしろこっちが良い感じ。
ただ公式にも書いてあったけど、言ってしまえばただの関数なので、どうにでも書けてしまう感はある。
調べてもこれという正解がいまいち分からないので、複雑なものをReact Hooksでやろうとしたらまだいろいろ躓きそう。
今後どうなるかは分からないけど、Reduxの方が今のところ参考になる記事も多い気がするので、変なやり方を覚えたりする心配は少なそう。
規模が大きかったり複雑な場合は大人しくreduxに身を委ねるのが良いのかもと思ったり。
Laravel8 + Sanctum + ReactでSPA認証
前提
Larave8 + ReactでSPA環境ができていること
この記事ではブログ内で作った続きでやります。 nochio12.hatenablog.com
基本的に公式通り進めます。
laravel.com
Sanctumではトークンでの認証とCookieを使った認証のどちらかを選ぶことができますが、
今回はCookieを使った認証を実装します。
インストール
$ composer require laravel/sanctum $ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" $ php artisan migrate
2つめのコマンドで設定ファイルとmigrationが作成されます。
ミドルウェアの追加
[Kernel.php]
+ use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; 'api' => [ + EnsureFrontendRequestsAreStateful::class, 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ],
モデルの修正
[Models/User.php]
+ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { - use HasApiTokens, Notifiable; + use HasFactory, HasApiTokens, Notifiable; }
CORS対策
[config/cors.php]
- 'paths' => ['api/*], + 'paths' => ['api/*', 'sanctum/csrf-cookie'], - 'supports_credentials' => false, + 'supports_credentials' => true,
実際の環境のドメインをenvに追記
[.env]
+ SANCTUM_STATEFUL_DOMAINS=127.0.0.1:8080
[config/sanctum.php]
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')),
ローカルでの開発で、ここにあるドメインであれば追記しなくて平気ですが
ポートを変えていたり、本番環境ではどっちみち必要。
調べると出てきますが、公式で言及がないため忘れそう。
認証処理
Api用の認証コントローラを作成
$ php artisan make:controller Api/Auth/LoginController
[Api/Auth/LoginController.php]
<?php namespace App\Http\Controllers\Api\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { use AuthenticatesUsers; /** * ログイン * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse * * @throws \Illuminate\Validation\ValidationException */ public function login(Request $request) { // バリデーション $this->validateLogin($request); $result = false; $status = 401; $message = 'ユーザが見つかりません'; $user = null; $credentials = $request->only('email', 'password'); if (Auth::attempt($credentials)) { // Success $result = true; $status = 200; $message = 'OK'; $user = Auth::user(); // ※古いトークン削除&新しいトークン生成 $user->tokens()->where('name', 'token-name')->delete(); $token = $user->createToken('token-name')->plainTextToken; } return response()->json(['result' => $result, 'status' => $status, 'user' => $user, 'message' => $message]); } /** * ログアウト * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function logout(Request $request) { Auth::logout(); $result = true; $status = 200; $message = 'ログアウトしました'; return response()->json(['result' => $result, 'status' => $status, 'message' => $message]); } }
フロントの実装
[js/bootstrap.js]
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+ window.axios.defaults.withCredentials = true;
ルーティングの追加
[app.js]
require('./bootstrap'); import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter, Route } from "react-router-dom"; import NavBar from "./NavBar"; import Home from "./Home"; import About from "./About"; import Auth from "./Auth"; // 追加 const App = () => { return ( <BrowserRouter> <NavBar /> <Route path="/about" component={About} /> <Route path="/auth" component={Auth} /> // 追加 <Route exact path="/" component={Home} /> </BrowserRouter> ); }; if (document.getElementById("app")) { ReactDOM.render(<App />, document.getElementById("app")); }
ログイン画面
[Auth.js]
import React, { useState, useEffect } from "react"; const Auth = () => { const [user, setUser] = useState(null); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); // ブラウザリロード時にログイン済みか判定 useEffect(() => { getUser(); }, [getUser]); // 認証ユーザを取得 const getUser = () => { axios.get("/api/user").then(res => { console.log('[getUser]ログイン済み'); console.log(res.data); setUser(res.data); }).catch(err => { console.log('[getUser]ログインしてません'); }) }; // ログイン const login = async e => { e.preventDefault(); // ログイン時にCSRFトークンを初期化 axios.get("/sanctum/csrf-cookie").then(response => { axios .post("/api/login", { email, password }) .then(res => { console.log(res.data); if (res.data.result) { console.log('[login]ログイン成功'); setUser(res.data.user); } else { console.log(res.data.message); console.log('[login]ログイン失敗'); } }) .catch(err => { console.log(err.response); console.log('[login]ログイン失敗'); }); }) }; // ログアウト const logout = () => { axios .get("/api/logout") .then(res => { setUser(null); }) .catch(err => { console.log(err); }); }; // ログインフォーム let form = ( <form onSubmit={login}> <label>email</label> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <label>password</label> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">Login</button> </form> ); // ユーザ情報 let userInfo = null; // 認証済みの場合、ログアウトボタンとユーザ情報を表示 if (user) { form = <button onClick={logout}>Logout</button>; userInfo = ( <div> <h2>User</h2> <div>name: {user.name}</div> <div>email: {user.email}</div> </div> ); } return ( <div> {form} {userInfo} <button onClick={getUser}>getUser</button> </div> ); }; export default Auth;
ビルドして確認。
ログインを試す
ログインのためのユーザーを作成します。 1つあればいいので、tinkerでサクッと作ります。
$ php artisan tinker >>> App\Models\User::factory()->create(['name' => 'hoge', 'email' => 'hoge@hoge.com', 'password' => Hash::make('password')]);
先ほど作成したフォームに
email : 'hoge@hoge.com
password : password
を打ち込み、ログインを試します。
ログインが成功し、画面にユーザー情報が表示されれば成功。 ログアウトせずにリロードをかけると勝手にログイン状態になるはず。
まとめ
パッケージがよしなにやってくれてラクチンですが
内部的に何が起きているのかはちゃんと理解して使いたいところ。
Laradock+Apache2のDocumentRootの設定でハマった
先日、LaradockでLaravelの開発環境を構築しました。
サーバはapache2にしたのですが、若干エラーにハマったのでメモ書き。
とりあえず普通に構築
最初の設定はこんな感じ
APP_CODE_PATH_HOST=../laravelproject APP_CODE_PATH_CONTAINER=/var/www/ <中略> APACHE_HOST_HTTP_PORT=800 APACHE_DOCUMENT_ROOT=/var/www/public
FROM webdevops/apache:centos-7
<VirtualHost *:80> ServerName servername.test DocumentRoot /var/www Options Indexes FollowSymLinks <Directory "/var/www/"> AllowOverride All <IfVersion < 2.4> Allow from all </IfVersion> <IfVersion >= 2.4> Require all granted </IfVersion> </Directory> </VirtualHost>
この設定で/var/www/直下にlaravelprojectが展開され、DocumentRootがpublicになると思いました。
エラー
「このサイトにアクセスできません」画面で接続できない。
ログを確認
$ docker logs -f laradock_apache2_1
無限にエラー出てる
2020-07-30 13:08:44,418 INFO spawned: 'apached' with pid 23 -> Executing /opt/docker/bin/service.d/httpd.d//10-init.sh 2020-07-30 13:08:44,507 INFO success: apached entered RUNNING state, process has stayed up for > than 0 seconds (startsecs) AH00526: Syntax error on line 119 of /etc/httpd/conf/httpd.conf: DocumentRoot '/var/www/html' is not a directory, or is not readable 2020-07-30 13:08:44,659 INFO exited: apached (exit status 1; not expected)
DocumentRootの/var/www/htmlがない? 設定が反映されていないのだろうか。
処置
とりあえず動く状態を目指す
APP_CODE_PATH_HOST=../laravelproject APP_CODE_PATH_CONTAINER=/var/www/project <中略> APACHE_HOST_HTTP_PORT=800 APACHE_DOCUMENT_ROOT=/var/www/project/public
<VirtualHost *:80> ServerName servername.test DocumentRoot /var/www/project/public Options Indexes FollowSymLinks <Directory "/var/www/project/public/"> AllowOverride All <IfVersion < 2.4> Allow from all </IfVersion> <IfVersion >= 2.4> Require all granted </IfVersion> </Directory> </VirtualHost>
$ docker-compose down $ docker-compose build apache2 $ docker-compose up -d apache2
http://localhost:800/
にアクセスしたら無事いつものLaravel画面が。
まとめ
いろいろ調べたけど、原因がいまいち掴めず。
もっといい方法があれば知りたい。
10/02追記
全然理解足りてなかった。
まあ今も理解できてないんですが、
サーバーにアクセスがあった時に見に行くRootはconfファイルの記述になるので、そっちの修正が必要だったと。
laradockの.envで設定する
APACHE_DOCUMENT_ROOT
は何をしているんだろうか。
Dockerfileに引数として渡されて環境変数として設定されている(?)けど、
その先にどう使われているが全く分からない。
Laravel8 + ReactのSPA環境構築
前提条件
・Laravel8のプロジェクトを作成し、初期画面表示まで完了している
環境構築した時の記事 nochio12.hatenablog.com
導入
Laravel8から公式にサポートされたJetstream+inertiaも気になりましたが、
どうもJetstreamはVueしか対応していないようなので、今回はスルーします。
Introduction | Laravel Jetstream
というわけで、Reactの導入手順はLaravel6辺りからと同じ手順です。
Reactのインストール
uiパッケージのインストール
$ composer require laravel/ui
Reactの導入
$ php artisan ui react --auth ... Please run "npm install && npm run dev" to compile your fresh scaffolding.
ビルド
$ npm install $ npm run dev
Reactの準備は一旦OK
Laravelのルーティングの設定
前の手順でbladeがいくつかできますが
SPAでは不要になるので消していきます。
また、ルーティングもReact側でやるので修正します。
削除前
resources
┗ views
┣ auth (削除)
┣ layouts - app.blade.php (views直下に移動)
┣ home.blade.php(削除)
┗ welcome.blade.php(削除)
削除後
resources
┗ views
┗ app.blade.php
app.blade.phpを修正
<body> <!-- divの中身を削除 --> <div id="app"> </div> </body>
resources/js/components/Example.js
- if (document.getElementById('example')) { - ReactDOM.render(<Example />, document.getElementById('example')); - } + if (document.getElementById('app')) { + ReactDOM.render(<Example />, document.getElementById('app')); + }
ここまでできたらビルド
$ npm run dev
web.phpを修正して、
どんなURLでもapp.blade.phpを返すようにします。
Route::get('{any}', function () {
return view('app');
})->where('any','.*');
ページを更新し、
ExampeComponentが表示されればOK
Reactの初期設定
app.jsに初期設定を記述
app.js
require('./bootstrap'); import React from 'react'; import ReactDOM from 'react-dom'; import Example from './components/Example'; if (document.getElementById('app')) { ReactDOM.render(<Example />, document.getElementById('app')); }
Exampleから描画関数を削除 Example.js
- if (document.getElementById('app')) { - ReactDOM.render(<Example />, document.getElementById('app')); - }
Reactのルーティング設定
ルーティングライブラリをインストール
$ npm install react-router-dom
jsディレクトリに
NavBar.js
Home.js
About.js
の3つを作成
NavBar.js
import React from 'react'; import { Link } from 'react-router-dom'; const NavBar = () => { return ( <div> <Link to="/">Home</Link> <Link to="/about">About</Link> </div> ); } export default NavBar;
Home.js
import React from 'react'; const Home = () => { return ( <div> Home </div> ); } export default Home;
About.js
import React from 'react'; const About = () => { return ( <div> About </div> ); } export default About;
app.jsを変更
import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter, Route } from "react-router-dom"; import NavBar from "./NavBar"; import Home from "./Home"; import About from "./About"; const App = () => { return ( <BrowserRouter> <NavBar /> <Route path="/about" component={About} /> <Route exact path="/" component={Home} /> </BrowserRouter> ); }; if (document.getElementById("app")) { ReactDOM.render(<App />, document.getElementById("app")); }
ルーターを使用する範囲をBrowserRouterで囲む必要があります。 Routeで、NavBar内のLinkのパスに対応するコンポーネントをセットしています。
$ npm run dev
して確認
画面とURLが切り替わるのが確認できればOK 後はゴリゴリ開発していくだけです。
【Vue】「This can lead to unexpected behavior when compiling on a filesystem with other case-semantic」エラー対処
エラー内容
普段あまり見ないけど、タイポした時に出るエラー。
google翻訳の結果
これは、他のケースセマンティックを使用してファイルシステムでコンパイルするときに予期しない動作を引き起こす可能性があります
原因と対策
例えば
/components/SampleComponent.vue
のようなファイルがあったとして
File1.vue
import SampleComponent from '@/components/SampleComponent';
File2.vue
import SampleComponent from '@/components/sampleComponent'; // sが小文字
のように複数箇所で別の記法でパスを書くと出てくるので、
File2.vueの方も
SampleComponent
に合わせる。
DockerでLaravel8(PHP7.4, Nginx, MySQL5.7)の最低限の環境構築
こちらを参考にさせていただきました。
Docker環境
ディレクトリ構成
project
┣ docker
┃ ┣ db
┃ ┣ nginx
┃ ┗ php
┣ server
┗docker-compose.yml
serverにlaravelプロジェクトが入る想定です。
各種ファイル作成
docker-compose.yml
version: '3' services: php: container_name: php build: ./docker/php volumes: - ./server:/var/www/html nginx: image: nginx container_name: nginx ports: - 50080:80 volumes: - ./server:/var/www/html - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - php db: image: mysql:5.7 container_name: db environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: database MYSQL_USER: docker MYSQL_PASSWORD: docker TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - ./docker/db/data:/var/lib/mysql - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf - ./docker/db/sql:/docker-entrypoint-initdb.d ports: - 53306:3306
ポートが絶対に被らないよう
nginx -> 50080
mysql -> 53306
に変更しています。
/nginx/default.conf
server { listen 80; index index.php index.html; root /var/www/public; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_param PATH_INFO $fastcgi_path_info; } }
php/Dockerfile
FROM php:7.4-fpm COPY php.ini /usr/local/etc/php/ #Composer install RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer RUN apt-get update \ && apt-get install -y \ git \ zip \ unzip \ vim RUN apt-get update \ && docker-php-ext-install pdo_mysql ENV COMPOSER_ALLOW_SUPERUSER 1 ENV COMPOSER_HOME /composer ENV PATH $PATH:/composer/vendor/bin WORKDIR /var/www RUN composer global require "laravel/installer"
[Date] date.timezone = "Asia/Tokyo" [mbstring] mbstring.internal_encoding = "UTF-8" mbstring.language = "Japanese"
コンテナ起動
$ docker-compose up -d
mysqlが起動失敗したので、エラーを確認
$ docker-compose up
バックグラウンド起動を辞めると、途中のエラーが分かります。
エラー内容
Operating system error number 22 in a file operation.
ぐぐったところ
docker-compose.ymlに
db: image: mysql:5.7 ...中略 command: --innodb-use-native-aio=0 #この1行追加
でOKそうです。
一度dbディレクトリのデータを削除し、再度コンテナ起動
$ docker-compose up -d Starting php ... done Starting db-host ... done Starting nginx ... done
無事起動完了
Laravelプロジェクト作成
作業コンテナに入る
$ docker-compose exec php bash /var/www# composer create-project --prefer-dist laravel/laravel .
しばらく待ちます。
Package manifest generated successfully. 73 packages you are using are looking for funding. Use the `composer fund` command to find out more! > @php artisan key:generate --ansi Application key set successfully.
成功文が出たら
http://localhost:50080にアクセス
Laravelのトップ画面が表示されればOK
DB作成
一度コンテナから出て、DBコンテナに接続します。
$ docker-compose exec db bash
コンテナ
/# mysql -u root -p // docker-compose.ymlのMYSQL_ROOT_PASSWORD Enter password: root // データベース作成 mysql> create database laravel; mysql> quit; /# exit;
次にLaravel側の設定をします。
.env
DB_CONNECTION=mysql # DB_HOST=127.0.0.1 DB_HOST=db DB_PORT=3306 DB_DATABASE=laravel # さっき作ったDB名 DB_USERNAME=root DB_PASSWORD=root
設定出来たら再びPHPコンテナへ
$ docker-compose exec php bash /var/www# cd html /var/www/html# php artisan migrate
無事マイグレーションが完了すればOK
まとめ
Laradock使ってて、重いし謎のエラー出るしで嫌になったので
今後はこの最小構成をベースにLaravel触っていきたい。
vue-cliでvue create後、npm run serveに失敗した時の対処法
環境
PC - Windows 10 Home node.js - 12.16.3 npm - 6.14.4
vueプロジェクト作成
まずはグローバルにvue-cliをインストール
> npm install -g @vue/cli
インストールを確認
> vue --version @vue/cli 4.4.6
適当に作ったディレクトリでプロジェクト作成 今回はvueprojectという名前で進めます。
> vue create .
いろいろ聞かれますが、とりあえずデフォルトで進めます。
Successfully created project vueproject. Get started with the following commands: $ npm run serve
と出れば成功。 言われた通り、サーバーを起動します。
> npm run serve
エラー発生
npm ERR! code ELIFECYCLE npm ERR! vueproject@0.1.0 serve: `vue-cli-service serve` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the vueproject@0.1.0 serve script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! C:\Users\user\AppData\Roaming\npm-cache\_logs\2020-07-03T03_35_30_681Z-debug.log
vue-cli-serviceがおかしいらしいけど、よく分からないのでエラー文でググります。
解決
ググって発見。 https://teratail.com/questions/219324
プロジェクトディレクトリから下記を削除する - node_modules - package-lock.json
その後、モジュールを再インストール
> npm install
完了したら、サーバー起動
> npm run serve App running at: - Local: http://localhost:8080/ - Network: http://192.168.2.107:8080/ Note that the development build is not optimized. To create a production build, run npm run build.
起動確認。
http://localhost:8080/ にアクセスし、下記の画面が表示されたらOK。
まとめ
別件でも同手順で対処したことあったし、モジュール入れ直しは割とあるあるなのかも。 エラー時にもっといろいろ出る場合は別の原因がありそう。