- 投稿日:2020-03-05T22:54:47+09:00
React Navigationについて�かなりザックリ解説する
- 投稿日:2020-03-05T22:42:03+09:00
React.jsについてかなりザックリ解説する
React.js(通称リアクト)とは
様々な記事でReactはライブラリだよとか、フレームワークだよとかって解説されててごっちゃになります。なので自分用に整理しておくこととします。
React.jsはFacebook製のJavaScriptライブラリです。フレームワークではありません。フレームワークはちゃんと別で名前があり、それがReact Nativeです。
Reactの特徴
Reactと言えばコンポーネントと言う単語がよく出てきます。コンポーネントとは部品のことで、「UIを独立した再利用可能な部分に分割したもの」のことを指します。
なんのこっちゃと思いますが、ボタン、フォーム、画面、ダイアログなどのそれぞれのUIを部品ごとに分けて開発することで、保守性に優れた開発が出来るようになるんだとか。
Reactの概要を学ぶなら
この記事がめちゃくちゃ参考になりそうです。
ちょっとずつ理解を深めていきましょう。
- 投稿日:2020-03-05T22:08:55+09:00
ReactでマークダウンをHTMLに変換する
ReactでマークダウンをHTMLに変換する方法について記載
1.react-markdownをインストール
npm install --save react-markdown2.以下のコードを記載
import React,{Component} from 'react'; import ReactMarkdown from 'react-markdown/with-html'; export default class MarkdownSample extends React.Component { constructor(props) { super(props) } getHTMLfromMarkdown(){ let markdown = '# 見出し 1\n' + '## 見出し 2\n' + '### 見出し 3\n' + '#### 見出し 4\n' + '---\n' + '- リスト 1\n' + '- リスト 2\n' + '- リスト 2-1\n' + '1. 番号付きリスト 1\n' + '2. 番号付きリスト 2\n' + '3. 番号付きリスト 3\n' + '[リンク](http://...)\n' + '**強調**\n' + '```ruby:filename.rb\n' + 'コード\n' + '```'; return markdown; } render() { return ( <div> <ReactMarkdown source={this.getHTMLfromMarkdown()} escapeHtml={false} /> </div> ) } }以上
参考
- 投稿日:2020-03-05T21:17:51+09:00
【react-native】setStateで値の反映が待てないあなた向け
はじめに
setStateをした後にその中の値を使った計算をしたいのに、反映が遅くて使えない・・・
なんて思ったことありませんか。私はありません。ということで対策案を考えました。
まずは事象から見てください。事象
こちらデモですが、「No1add」「No2add」ボタンを押下することでそれぞれの数字に値を1足す。
その後、合計値にという流れを実装したいのですが、うまくいきません。
合計値がボタンを押す前のNo1とNo2の合計値になってしまいます。ソースは以下です。
App.jsimport React from 'react'; import { StyleSheet, View, Button, Text } from 'react-native'; export default class App extends React.Component { state = { num1: 0, num2: 0, sum: 0, } render() { const onPressNo1 = () => { this.setState({ num1: this.state.num1 + 1 }) // 合計点を随時更新する calcSum(); } const onPressNo2 = () => { this.setState({ num2: this.state.num2 + 1 }) // 合計点を随時更新する calcSum(); } const calcSum = () => { this.setState({ sum: this.state.num1 + this.state.num2 }) } return ( <View style={styles.container}> <View style={styles.row}> <Button title="No1add" onPress={() => onPressNo1()} /> <Text style={styles.textStyle}>No1: {this.state.num1}</Text> </View> <View style={styles.row}> <Button title="No2add" onPress={() => onPressNo2()} /> <Text style={styles.textStyle}>No2: {this.state.num2}</Text> </View> <Text style={styles.textStyle}>No1 + No2 = {this.state.sum}</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, row: { flexDirection: "row", alignItems: "center" }, textStyle: { fontSize: 20, } });原因はsetStateで値が反映されるのは画面が再描画されるタイミングのため、合計値の計算のタイミングではまだnum1とnum2の値が更新されていないため。
以下、対策案です。
他にもあれば教えてください。対策案①:stateに値を直接いれる
これはあまりいけてないですが、パワープレイしたいならこちらが良いでしょう。
App.jsimport React from 'react'; import { StyleSheet, View, Button, Text } from 'react-native'; export default class App extends React.Component { state = { num1: 0, num2: 0, sum: 0, } render() { var varNum1 = this.state.num1; var varNum2 = this.state.num2; var varSum = this.state.sum; const onPressNo1 = () => { varNum1 = this.state.num1 + 1 this.setState({ num1: varNum1 }) // 合計点を随時更新する calcSum(); } const onPressNo2 = () => { varNum2 = this.state.num2 + 1 this.setState({ num2: varNum2 }) // 合計点を随時更新する calcSum(); } const calcSum = () => { varSum = varNum1 + varNum2 this.setState({ sum: varSum }) } return ( <View style={styles.container}> <View style={styles.row}> <Button title="No1add" onPress={() => onPressNo1()} /> <Text style={styles.textStyle}>No1: {this.state.num1}</Text> </View> <View style={styles.row}> <Button title="No2add" onPress={() => onPressNo2()} /> <Text style={styles.textStyle}>No2: {this.state.num2}</Text> </View> <Text style={styles.textStyle}>No1 + No2 = {this.state.sum}</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, row: { flexDirection: "row", alignItems: "center" }, textStyle: { fontSize: 20, } });変数を新たに用意し、そこに値を入れ即時反映させていくやり方です。
いけてないですが、確実に理想的な実装ができます対策案②:再描画タイミングで計算しちゃおう
sumをstateでもつことなんてやめましょうw
App.jsimport React from 'react'; import { StyleSheet, View, Button, Text } from 'react-native'; export default class App extends React.Component { state = { num1: 0, num2: 0, } render() { const onPressNo1 = () => { this.setState({ num1: this.state.num1 + 1 }) } const onPressNo2 = () => { this.setState({ num2: this.state.num2 + 1 }) } return ( <View style={styles.container}> <View style={styles.row}> <Button title="No1add" onPress={() => onPressNo1()} /> <Text style={styles.textStyle}>No1: {this.state.num1}</Text> </View> <View style={styles.row}> <Button title="No2add" onPress={() => onPressNo2()} /> <Text style={styles.textStyle}>No2: {this.state.num2}</Text> </View> <Text style={styles.textStyle}>No1 + No2 = {this.state.num1 + this.state.num2}</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, row: { flexDirection: "row", alignItems: "center" }, textStyle: { fontSize: 20, } });まとめ
独学しているとsetStateの正しい使い方これであってるのかな・・・と不安になります。
一旦まとめて見ました。他に案がある人募集しています。
教えて欲しいです・・・
- 投稿日:2020-03-05T18:52:04+09:00
第4回 2020年版 既存のウェブサイトに React を追加する
1. 概要
Reactを大規模に利用することが難しい場合、少しだけReactを取り入れてみて、徐々に適用範囲を広げていくのが良いです。
Reactでは既存のページに部分的に利用することが可能であり、今回はその方法について説明します。2. 前提条件
作業日時
- 2020/3/5
ソフトウェアのバージョン
ソフトウェア バージョン webpack 4.42.0 babel 7.8.6 ts-loader 6.2.1 react 16.13.0 react-dom 16.13.0 typescript 3.8.3 3. シンプルな利用方法
3.1. 環境の準備
create-react-app
は利用せず、自分でWebpack等の設定ファイルを準備する。
最初にyarn init
でpackage.json
を作成し、その後、必要なパッケージをインストールする。webpack、babel、typescriptの他にhtml確認用のwebpack-dev-serverをインストールする。
$ yarn init $ yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin $ yarn add -D @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/register $ yarn add -D typescript ts-loaderReact関連もインストールする。
$ yarn add react react-dom $ yarn add -D @types/react @types/react-dom $ yarn add @material-ui/core @material-ui/icons3.2.設定ファイルの作成
3.2.1.Webpackの設定ファイル
webpackのコンフィグファイルを作成します。
$ touch webpack.config.js設定ファイルには以下のように記述します。
エントリポイントはindex.jsx
で、出力ファイルはbundle.js
にしています。webpack.config.jsrequire('@babel/register'); // development.jsでES6を使えるようにする const path = require('path') const src = path.resolve(__dirname, 'src'); const dist = path.resolve(__dirname, 'dist'); module.exports = { mode: 'development', // mode: "production", entry: src + '/index.jsx', output: { path: dist, filename: 'bundle.js' }, module: { rules: [ { test: /\.jsx$/, exclude: /node_modules/, use: [ { loader: "babel-loader", // Babel を利用する options: { // Babel のオプションを指定する presets: [ "@babel/preset-env", // プリセットを指定することで、ES2020 を ES5 に変換 "@babel/react" // React の JSX を解釈 ] } } ] } ] }, resolve: { extensions: ['.js', '.jsx'] }, plugins: [] }3.2.2. Typescriptの設定
Typescriptの設定ファイルも作成します。
tsconfig.json{ "compilerOptions": { "sourceMap": true, "target": "es5", /* tsを書くときにどのversionのESが対象か: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "es2015" /* どのversionのESを生成するか: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ /* Strict Type-Checking Options */ "strict": true, /* すべての strict タイプ・オプション(noImplicitAny, strictNullChecks, noImplicitThis(thisの型チェック), alwaysStrict(strictモードのjs出力)) を 有効化する */ "noImplicitAny": true, /* any型の使用不可 */ "strictNullChecks": true, /* nullable型以外でnullを許容しない */ /* Additional Checks */ "noUnusedLocals": true, /* 未使用の変数を許容しない */ "noUnusedParameters": true, /* 未使用の変数を許容しない */ "noImplicitReturns": true, /* メソッド内で返り値の型があっているかをチェック */ /* Module Resolution Options */ "moduleResolution": "node", /* http://js.studio-kingdom.com/typescript/handbook/module_resolution 参照 */ "esModuleInterop": true /* ESModuleと同じ動作をする. */ } }3.3. サンプルの作成
3.3.1. HTML に DOM コンテナを追加する
Reactを追加したい HTML ファイルを用意します。
React で描画したい箇所に空の
<div>
要素を追加し、ユニークな id 属性を指定します。また、<script src="bundle.js" charset="utf-8"></script>
を追加し、React コンポーネントのJavascriptを読み込みます。これで、後から
<div>
要素にReact コンポーネントを描画する準備ができました。index.html<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <title>React App</title> </head> <body><noscript>You need to enable JavaScript to run this app.</noscript> <div id="app" /> </body> <script src="bundle.js" charset="utf-8"></script> </html>3.3.2. Reactコンポーネントを作成する
次にReactコンポーネントを作成します。
src/index.tsximport React from 'react'; import {render} from 'react-dom'; function App() { return ( <div> <h1>Hello React!</h1> </div>); } render(<App/>, document.getElementById('app'));3.3.3. ビルド
Webpackを使って上記のコードをビルドします。
成功すれば、dist/bundle.js
が生成されます。$ ./node_modules/.bin/webpackwebpack-dev-serverを起動して、ブラウザからlocalhost:8080にアクセスしてみます。
Hello React!
と表示されれば成功です。$ ./node_modules/.bin/webpack-dev-server4. 複数のコンポーネントを利用する方法
先程のサンプルでは一つのRreactコンポーネントを利用しただけですが、次はReactコンポーネントを利用する方法について説明します。
4.1. 設定ファイルの修正
複数のファイルを一度に生成できるように、
webpack.config.js
を修正します。
entry
に複数のコンポーネントを列挙します。また、output
のfilename
を[name].bundle.js
とすることで、それぞれ別のファイルが出力されるようにします。webpack.config.jsrequire('@babel/register'); // development.jsでES6を使えるようにする const path = require('path') const src = path.resolve(__dirname, 'src'); const dist = path.resolve(__dirname, 'dist'); module.exports = { mode: 'development', // mode: "production", entry: { appbar: src + '/index.tsx', content: src + '/content.tsx' }, output: { path: dist, filename: '[name].bundle.js' }, module: { rules: [ { // 拡張子 .ts の場合 test: /\.tsx?$/, exclude: /node_modules/, // TypeScript をコンパイルする use: [ // 下から順に処理される { loader: "babel-loader", // Babel のオプションを指定する options: { presets: [ // プリセットを指定することで、ES2020 を ES5 に変換 "@babel/preset-env", // React の JSX を解釈 "@babel/react" ] } }, { loader: "ts-loader" } ], } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, plugins: [] };4.2. サンプルアプリの作成
htmlファイルと、ヘッダとコンテンツのReactコンポーネントの3つのファイルを作成します。
最初にヘッダ部分の描画用のReactコンポーネントを作成する。
<StylesProvider>
はMaterial-UIで自動生成するクラス名が重複しないようにするため、createGenerateClassName
での衝突を避けるためのプリフィックス等の指定を行う。index.tsximport React from 'react'; import ReactDOM, { render } from 'react-dom'; import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'; import { AppBar, Toolbar, Typography, IconButton, Avatar } from '@material-ui/core'; import red from '@material-ui/core/colors/red'; // Material-UIアイコン取得 import NotificationImportantIcon from '@material-ui/icons/NotificationImportant'; import MenuIcon from "@material-ui/icons/Menu"; const generateClassName = createGenerateClassName({ productionPrefix: 'a', seed: 'appbar', // classNameが重複しないようにするために設定 }); // スタイルを適用する const useStyles = makeStyles((theme: Theme) => createStyles({ headerLogo: { color: "inherit", marginRight: 20, }, headerTitleStyle: { flexGrow: 1, color: "inherit", }, menuButton: { color: "inherit", padding: '8px', }, avatar: { margin: '8px', backgroundColor: red[500], } }), ); function MyAppBar() { // CSSを適用する。 const classes = useStyles(); return ( <StylesProvider generateClassName={generateClassName}> <div> <AppBar position='static' aria-label="Global Navi"> <Toolbar> <Typography className={classes.headerLogo} variant="subtitle1">My Sample App</Typography> <Typography className={classes.headerTitleStyle} variant="subtitle1" >Material UI test</Typography> <NotificationImportantIcon></NotificationImportantIcon> <IconButton className={classes.menuButton} aria-label="Menu"> <Avatar className={classes.avatar}></Avatar> </IconButton> <IconButton aria-label="SideMenu"> <MenuIcon /> </IconButton> </Toolbar> </AppBar> </div> </StylesProvider> ) } ReactDOM.render(<MyAppBar />, document.getElementById('appbar'));次にコンテンツ部分の描画用のReactコンポーネントを作成する。
content.tsximport React from 'react'; import ReactDOM, { render } from 'react-dom'; import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'; import { Typography, IconButton, Avatar, Paper, Tab, Tabs, Box, Card, CardHeader, CardMedia, CardContent } from '@material-ui/core'; import red from '@material-ui/core/colors/red'; // Material-UIアイコン取得 import MoreVertIcon from '@material-ui/icons/MoreVert'; const generateClassName = createGenerateClassName({ productionPrefix: 'b', seed: 'content', // classNameが重複しないようにするために設定 }); // スタイルを適用する const useStyles = makeStyles((theme: Theme) => createStyles({ avatar: { margin: '8px', backgroundColor: red[500], }, card: { textAlign: 'center', maxWidth: 400, }, media: { height: 0, paddingTop: '56.25%', // 16:9 }, actions: { display: 'flex', }, expand: { transform: 'rotate(0deg)', marginLeft: 'auto', transition: theme.transitions.create('transform', { duration: theme.transitions.duration.shortest, }), }, expandOpen: { transform: 'rotate(180deg)', }, }), ); interface TabPanelProps { children?: React.ReactNode; index: any; value: any; } function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props; return ( <Typography component="div" role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other} > {value === index && <Box p={3}>{children}</Box>} </Typography> ); } function Content() { // CSSを適用する。 const classes = useStyles(); const [value, setValue] = React.useState(0); const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; return ( <StylesProvider generateClassName={generateClassName}> <div> <Paper square> <Tabs value={value} indicatorColor="primary" textColor="primary" onChange={handleChange} variant="scrollable" scrollButtons="auto" aria-label="scrollable auto tabs example" > <Tab label="TOP" /> <Tab label="お買い物" /> <Tab label="ファッション" /> <Tab label="グルメ" /> <Tab label="おでかけ" /> <Tab label="スポーツ" /> <Tab label="映画・ドラマ" /> </Tabs> <TabPanel value={value} index={0}> <Card className={classes.card}> <CardHeader avatar={<Avatar aria-label="Recipe" className={classes.avatar}>R</Avatar>} action={ <IconButton> <MoreVertIcon /> </IconButton> } title="キットカット4種セット(毎日のナッツ&クランベリー[パウチ36g/ルビー パウチ31g]" subheader="2020年3月5日" /> <CardMedia className={classes.media} image="/img/paella.jpg" title="Paella dish"/> <CardContent><Typography component="p">カードの説明</Typography></CardContent> </Card> </TabPanel> <TabPanel value={value} index={1}> Item Two </TabPanel> <TabPanel value={value} index={2}> Item Three </TabPanel> <TabPanel value={value} index={3}> Item Four </TabPanel> <TabPanel value={value} index={4}> Item Five </TabPanel> <TabPanel value={value} index={5}> Item Six </TabPanel> <TabPanel value={value} index={6}> Item Seven </TabPanel> </Paper> </div> </StylesProvider> ) } ReactDOM.render(<Content />, document.getElementById('content'));最後にhtmlファイルを作成する。
ヘッダとコンテンツ用のReactコンポーネントを読み込むため、Reactを描画するにidを指定し、appbar.bundle.js
とcontent.bundle.js
のJavascriptを読み込む。index.html<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <title>React App</title> </head> <body><noscript>You need to enable JavaScript to run this app.</noscript> <header> <h1>ページタイトル<h1> </header> <div id="appbar"></div> <div id="content"></div> <script src="appbar.bundle.js" charset="utf-8"></script> <script src="content.bundle.js" charset="utf-8"></script> <footer> <nav> <a href="index.html">トップページ</a> <a href="about.html">このサイトについて</a> </nav> <p>Copyright 2020</p> </footer> </body> </html>4.3. ビルドする
ビルドして、サーバーで表示します。
$ ./node_modules/.bin/webpack $ ./node_modules/.bin/webpack-dev-server以下の画面が表示されたら成功です。
5. 最後に
今回は既存のhtmlにReactを部分的に利用する方法について説明しました。
既に運用しているサービスがある場合、全面的なSPA化は困難ですが、部分的に置換えていくことで、徐々にReactに移行していくことが可能だと思います。6. 過去の記事
過去のReactに関する記事です。
- 投稿日:2020-03-05T14:15:42+09:00
個人サービスの運用コストがどのぐらいかかるか検証した
はじめに
個人サービスをリリースして1年半、ようやく利益が出てきたので、運用した知見と運用コストについて公開したいと思います。
運用しているサービスについて
洋楽にまつわる最新情報を更新、配信、投稿出来る音楽総合メディアです。
流入
PV
おかげさまで10万PVほどありました。特にバズったということもなく、毎日同じようなアクセス数がありました。
流入元
恥ずかしながらSEOの影響でorganic searchがほぼ数を占めています。自分のブログやTwitterでアウトプットはしているものの、やり方が悪いのか、そもそもセンスがないのか、あまり効果が出ていません。逆に流入の見込みが立てやすいので計画が立てやすいかもしれません(後付け
諸経費
サーバー代 + データベース諸々
2月は¥2,000ちょっとですみました。AzureのApp Service on Linuxというやつを使っていて、現在値下げ中なのでこの価格になります。余談ですが、昨年の12月末に値下げ終了告知をしていたのですが、今年の1月末まで延び、さらには今年の7月末までと値下げ期間がどんどん延びています。このまま続いてくれれば良いのですが。。Azureは若干高い印象があるので値下げが終わったらDocker化して他のサービスに移行してやろうかと思ってます。
ドメイン代
お名前.comを利用しています。大体¥1,500ぐらいです。年々高くなっているのはなんでですかね?
SSL
Let's Encryptを利用しています。無料なのでお金掛かってません。
開発費
全て自分でやっているので0円ですが、全て外注したらいくらかかるのだろう。。
キャンペーン費
サービスの性質上、ユーザーが投稿してくれる形で成り立ってるので毎月数名にamazonギフト券を還元しています。楽しんで記事を書いてくれる方がほとんどなのですが、運営している本人が「このサービス使ってもあまり利益ないな」と感じているのでその懺悔としてお気持ち程度に。。
負荷
平均応答時間
PageSpeed Insightsでは200ミリ秒以下は遅いと判断されるので、まぁ許容範囲かと思います。ところどころ負荷が高いところがみられますが、1日1回定期タスクが動いている影響です。
サーバーの応答時間は 200 ミリ秒以下に抑える必要があります。
CPU
10%以下に抑えられているので大丈夫でしょう。ところどころ負荷が高いところがみられますが、1日1回定期タスクが動いている影響です。
メモリ
64%とまぁまぁ使っている状況です。まだプランを変えずに踏ん張れるレベルかと思います。
データベース
DTUというAzure特有の単位なのですが、1%程度なのでほぼほぼ問題ないかと思います。
DTU(Database Transaction Unit)という単位で定義され、CPU、メモリ、I/Oの組み合わせからなります。
DTUが大きくなるほど、コンピューティングリソースが多く使えるようになり、標準で付属するストレージ、追加できるストレージも大きくなります。後述するように、ダウンタイムを発生させずにスケールの変更ができます。収益
GoogleAdSenseのみの収益となります。金額は
6,188円
でした。
※ キャプチャ載せて違反とか食らったら困るので載せらせません?♀️
まとめ
今は利益出ていますが、これまでは収益がほぼなかったので諸経費がそのまま掛かってました。やっと利益が出たので定期的にユーザーさんに大して還元出来る状況になったのはよかったと思います。私は「サービスを使うユーザーの利益がなければ陳腐化する」というポリシーを持ってサービスを運用しているので、ユーザーの利益が「お金」である以上は儲かるまでの敷居が高くなります。なので、利益をどこに持っていくかが鍵となりますが、そんなこと気にせずにこのぐらいのお金を投資出来るよーという方に個人サービスを作って欲しいと思います!
- 投稿日:2020-03-05T13:42:36+09:00
Rails + React + AjaxでCRUDのサンプルプロジェクト [Hello World]
React初心者が公式サイトで基礎を学んだ後に作るReact + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。
Twitterのように「つぶやき」を投稿可能で編集/削除もできます。動作確認はChrome、FireFox、Microsoft Edge、IE11です。恐らくマックさんのブラウザでも動作するはずです。
DEMO
https://www.petitmonte.com/rails-demo/react_crud
ソース一式
https://github.com/TakeshiOkamoto/mpp_react_crud
※学習用の為、ライセンスはパブリックドメイン
- 投稿日:2020-03-05T13:04:20+09:00
Reactのプロキシを通すとSSE (Server-sent events)のレスポンスを受け取れない
React、というかreact-scriptsはプロキシ機能を備えています。
このプロキシ、基本的にはウェブAPIのために用います。React自体はlocalhost:3000で実行し、ウェブAPIはlocalhost:8080で実行する場合、localhost:3000にウェブAPIのリクエストを投げるとlocalhost:8080にプロキシする、という具合ですね1。
使用方法も簡単で、
package.json
に下記のようなフィールドを付け加えるだけで動作します。{ // ... "proxy": "http://localhost:8080", // ... }ただこのプロキシを通してSSE (Server-sent events)なウェブAPIにリクエストを投げると、レスポンスのヘッダーなどは受け取れるものの、本文が受け取れません。悲しいね……。
という事実を述べて終わりたいわけじゃなく、なぜそうなるのか、どうすれば解決できるのかまとめてみました。
手っ取り早く解決したい方へ
「理由はどうでもいいから一刻も早く解決したい!」という方はここ読むだけで大丈夫です。
Reactアプリケーションのソースコードが置いてあるルートに以下の内容を
setupProxy.js
というファイル名で保存してください。ちなみに/apiへのリクエストをlocalhost:8080にプロキシする、という内容ですので、その辺は適宜変更してください。const proxy = require('http-proxy-middleware'); module.exports = (app) => { app.use('/api', proxy({ target: 'http://localhost:8080', onProxyReq: (proxyReq, req) => { if (req.headers['accept'] === 'text/event-stream') { req.headers['accept-encoding'] = 'identity'; } } })); };このファイルを置いた後、react-scripts restartを行えば解決すると思います。
原因
react-scriptsは内部でwebpack-dev-serverを利用しています。webpack-dev-serverはさらにExpress.jsを内包しており、そのミドルウェアで様々な機能を実現しています。プロキシもそのひとつです2。
そして、デフォルトでレスポンスの圧縮も行っています3。圧縮もExpress.jsのミドルウェアで実現されており、compressionがその実体です。
で、原因はずばりこの圧縮にあります。この件はすでにOption to disable the compression for the webpack dev server · Issue #7847 · facebook/create-react-appなどで言及されています。
対処
さきほどのIssueにもコメントとして残されていますが、SSEなウェブAPIのレスポンスヘッダーに
Cache-Control: no-transform
を付与すればこの問題は解決します。なぜ解決するかはcompressionミドルウェアのソースコードを眺めると簡単に分かります。
compressionミドルウェアは以下のいずれかに該当するときに圧縮を行いません。
- filter関数がfalseを返したとき
- shouldTransform関数がfalseを返したとき
- Content-Lengthが閾値(デフォルト: 1024)未満のとき
- 既に何かしらの圧縮が施されていたとき
- リクエストメソッドがHEADのとき
- Accept-Encodingヘッダーにidentityが含まれているとき(実際は重みに依る)
となっているので、
Cache-Control: no-transform
とすると、2番目の条件に該当して圧縮が行われない、というわけです。もう一歩踏み込んだ対処
Cache-Contro: no-transform
が妥当なケースならそれで良いはずです。ただ、
Cache-Control: no-transform
はCache-Control - HTTP | MDNでも触れられている通り、プロキシにおけるあらゆる変換の禁止を宣言するものです。それにより圧縮も行われなくなるのですが、圧縮は許容しないが、他の変換は許容するという場合には不適切です。まーーーーーーーいろいろ考慮することはあると思うのですが、今回に限れば圧縮が悪さしているので、圧縮にのみアプローチできるのが最も影響範囲が小さいと思います。そうなると
Cache-Control: no-transform
はやや影響範囲が大きいかな……、と思う。ちょっと強引な気もするけど。SSEであれば実際はあらゆる変換を禁止するのが妥当な気もするけど。とりあえず圧縮にのみアプローチしたい、となった場合、やはり着目すべきはAccept-Encodingヘッダーでしょう。実際compressionミドルウェアもAccept-Encodingヘッダーにidentityが含まれていれば圧縮を行わないので、対処自体は妥当だと思います。
というわけでAccept-Encodingヘッダーを付けよう!と思ったけど、SSEのクライアントとして使うであろうEventSourceはリクエストヘッダーを指定できない!ていうかそもそもAccept-Encodingヘッダーはプログラムによる指定を禁止されている!
となるので、先にも挙げたこのコードになるのです。
const proxy = require('http-proxy-middleware'); module.exports = (app) => { app.use('/api', proxy({ target: 'http://localhost:8080', onProxyReq: (proxyReq, req) => { if (req.headers['accept'] === 'text/event-stream') { req.headers['accept-encoding'] = 'identity'; } } })); };react-scriptsはsetupProxy.jsを読み込むようになっており、そこで詳細なプロキシの設定を行えます。
そのとき、http-proxy-middleware(実際は内包しているhttp-proxy)にてプロキシ時のイベントを捉えることが出来ます。そこで、EventSourceからのリクエスト、つまりAccept: text/event-streamのときに限りAccept-Encoding: identityとしてあげます。
するとcompressionミドルウェアはそれを考慮し、圧縮を行いません。
すごくめでたしな感じしません?な?なーーー!?とりあえず私はこうしています。
また別の対処
CORSを使え!
- 投稿日:2020-03-05T11:48:59+09:00
React+Express+WebpackでSSRするときのキャッシュ対策
SPAの場合
webpackでbuildするとき、SPAであればキャッシュ対策は簡単です。
webpack.client.jsmodule.exports = { entry: ["@babel/polyfill", "./src/index.jsx"], output: { path: path.resolve("dist"), filename: "[name]-[hash].js" }, }と、outputに
[hash]
をつけるだけ。
あとはwebpackでほぼ必須なHtmlWebPackPlugin
が参照できるようHTMLをビルドしてくれる。以下のようなHTMLがビルドされます。
index.html<!DOCTYPE html> <html lang="ja"> <head> <base href="/"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id="index"></div> <script type="text/javascript" src="main-09fede0d434d3225ac66.js"></script> </body> </html>hash値がbundleファイルに付くので、これがキャッシュバスターになります。
SSRするときは
SSRでキャッシュ対策するとき、hashをつける方法でやるなら、サーバ側でレンダリングするHTMLでhash付きのbundleファイルを正しく参照できないといけないです。
例えばclientは
webpack.client.js
で、server(express)はwebpack.server.js
でビルドするとして、hashを共有する必要があります。
webpack.client.js
の設定は上記のままで、サーバサイドのエントリーポイントを以下のようにしてみました。server.jsimport express from 'express'; import { JSDOM } from 'jsdom'; import path from 'path'; ... const app = express(); ... // webpack.common.jsでビルドされた、main.jsのハッシュ付きパスを取得する(キャッシュバスター) let bundlePath; JSDOM.fromFile(path.resolve("dist", "index.html")).then(dom => { const document = dom.window.document; const script = document.querySelector('script[type="text/javascript"]'); bundlePath = script.src.replace("file:///", ""); }); app.all("*", (req, res, next) => { req.bundlePath = bundlePath; next(); }); ...jsdomを利用して、dom操作で、先に
webpack.client.js
でビルドされているdist/index.html
の中からscriptタグ内のsrcを、強引ですが持ってきてreq
オブジェクトに入れておきます。
script.src
はfile://
がついて取得されるので.replace
します。そして、SSRしている部分で、
render.js... const html = ` <!doctype html> <html> <head> <base href="/" /> </head> <body> <div id="index">${app}</div> <script type="text/javascript" src="${req.bundlePath}"></script> </body> </html> `;などとし、client -> serverの順でビルドすれば、SSRでもclientとserverでhash値の共有ができます。
curlしてみてdist内のjsファイルと見比べると、正しくできていることがわかります。
curl -H 'Cache-Control: no-cache' http://localhost/path/to/app <!doctype html> <html> <head> <base href="/" /> </head> <body> <div id="index"> ... </div> <script type="text/javascript" src="main-09fede0d434d3225ac66.js"></script> </body> </html>補足1
サーバサイドのエントリーポイントをビルドするときに、canvasがどうのってエラー出るかもしれないですが、
webpack.server.jsmodule.exports = { target: "node", entry: "./src/server.js", output: { path: path.resolve("dist"), filename: "server.js" }, externals: { canvas: "commonjs canvas" }, ... }のように
externals
にcanvas入れたらビルド通りました。補足2
依存関係を入れずに対応することもできます。
正規表現で取得します。server.jsimport fs from "fs"; ... const html = fs.readFileSync(path.resolve("dist", "index.html"), {encoding: "utf-8"}); const scriptTag = html.match(/<script(.|\s)*?><\/script>/i)[0]; app.all("*", (req, res, next) => { req.scriptTag = scriptTag; next(); });このscriptTagをhtmlに挿入するだけ。
こっちのほうがいいかも。
- 投稿日:2020-03-05T10:45:56+09:00
React Native で画面遷移やタブメニュー【react-navigation 5.x】
react-navigation とは
react-navigation とは、React Native アプリのルーティングで使える便利なライブラリです。
画面遷移や、タブバー、そしてドローワー(横からすっとでてくるメニュー)などを利用できるので非常に便利。
4.x->5.x のメジャーバージョンアップに伴い、コンポーネントベースになりました。今回は、5.x の使い方を紹介します。
インストール
# ライブラリのインストール yarn add @react-navigation/native # dependencies のインストール yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-viewApp.tsx
import { NavigationContainer } from '@react-navigation/native'; import React, { useEffect } from 'react'; import HomeNavigator from './Home'; import NavigationService from './navigation-service'; const App: React.FunctionComponent = () => { return ( <NavigationContainer> <HomeNavigator /> </NavigationContainer> ); }; export default App;これで準備は完了です。
画面遷移の実装
ライブラリをインストールします。
yarn add @react-navigation/stackサンプルコードは以下のようになります。
import { createStackNavigator } from '@react-navigation/stack'; import React from 'react'; import { Alert, Button } from 'react-native'; const Stack = createStackNavigator(); const HomeNavigator = () => ( <Stack.Navigator screenOptions={{ headerTintColor: ..., headerStyle: { backgroundColor: ..., }, }} > <Stack.Screen name="Home" component={Home} options={{ headerRight: () => ( <Button onPress={() => Alert.alert('This is a button!')} title="Info" color="#fff" /> ), }} /> <Stack.Screen name="Details" component={Details} /> </Stack.Navigator> ); ... export default HomeNavigator;最初に
createStackNavigator
でスタックを定義しています。
作成したスタックはStack.Navigator
,Stack.Screen
の形で使用します。
screenOptions
には、このナビゲーター全体に適用するレイアウトを定義することができます。
個々のスクリーンに適用するレイアウトはoptions
に記載します。
component
で画面の要素を見ていきます。
二種類の書き方ができます。const Home: React.FunctionComponent = () => { const navigation = useNavigation(); return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Button title="Go to Details" onPress={() => navigation.navigate('Details')} /> </View> ); };const Home: React.FunctionComponent = ({ navigation }) => { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Button title="Go to Details" onPress={() => navigation.navigate('Details')} /> </View> ); };
navigation
を渡す方法は、react-navigation 4.x
までと変わりませんね。
props
として渡す方法の型定義が面倒になってしまったため、私は Hooks の方を使ってます。ボタンをクリックすると、 Details に遷移します。
タブメニューの実装
ライブラリをインストールします。
yarn add @react-navigation/bottom-tabsサンプルコードは以下のようになります。
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import React, { useLayoutEffect } from 'react'; import Ionicons from 'react-native-vector-icons/Ionicons'; const Tab = createBottomTabNavigator(); const TabNavigator = ({ navigation, route }) => { useLayoutEffect(() => { navigation.setOptions({ headerTitle: route.state ? route.state.routes[route.state.index].name : route.params?.screen || 'Home', }); }, [navigation, route]); return ( <Tab.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { let iconName; if (route.name === 'Home') { iconName = focused ? 'ios-information-circle' : 'ios-information-circle-outline'; } else if (route.name === 'Settings') { iconName = focused ? 'ios-list-box' : 'ios-list'; } return <Ionicons name={iconName} size={size} color={color} />; }, })} tabBarOptions={{ activeTintColor: 'tomato', inactiveTintColor: 'gray', }} > <Tab.Screen name="Home" component={Home} /> <Tab.Screen name="Settings" component={Settings} /> </Tab.Navigator> ); }; ... export default TabNavigator;
createBottomTabNavigator
でタブを作成。
Tab.Navigator
,Tab.Screen
で実際の内容を定義します。
screenOptions
,tabBarOptions
の代わりに、カスタムのタブメニューを利用することも可能です。tabBar={(props) => <TabBar {...props} />}
で定義します。
TabBar
は以下のような形で書くことができます。import React from 'react'; import { Dimensions, Text, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; const { width: windowWidth } = Dimensions.get('window'); const TabBar = ({ state, descriptors, navigation }) => { const tabWidth = windowWidth / state.routes.length; return ( <View style={{ flexDirection: 'row', width: windowWidth, height: 64 }}> {state.routes.map((route, index) => { const { options } = descriptors[route.key]; const label = options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined ? options.title : route.name; const isFocused = state.index === index; const onPress = () => { const event = navigation.emit({ type: 'tabPress', target: route.key, }); if (!isFocused && !event.defaultPrevented) { navigation.navigate(route.name); } }; const onLongPress = () => { navigation.emit({ type: 'tabLongPress', target: route.key, }); }; return ( <TouchableOpacity key={route.key} accessibilityRole="button" accessibilityStates={isFocused ? ['selected'] : []} accessibilityLabel={options.tabBarAccessibilityLabel} onPress={onPress} onLongPress={onLongPress} style={{ flex: 1, width: tabWidth, paddingTop: 10, }} > <Text style={{ color: isFocused ? '#673ab7' : '#222', alignSelf: 'center', }} > {label} </Text> </TouchableOpacity> ); })} </View> ); }; export default TabBar;スライドメニューの実装
ライブラリをインストールします。
yarn add @react-navigation/drawerサンプルコードは以下のようになります。
import { createDrawerNavigator } from '@react-navigation/drawer'; import React from 'react'; const Drawer = createDrawerNavigator(); const DrawerNavigator = () => { return ( <Drawer.Navigator initialRouteName="Home"> <Drawer.Screen name="Home" component={Home} /> <Drawer.Screen name="Notifications" component={Notifications} /> </Drawer.Navigator> ); }; ... export default DrawerNavigator;おまけ
スクリーンのなかで画面遷移をする場合、
props
かuseNavigation
からnavigation
をうけとって使用する方法を紹介しました。では、自前の関数など、スクリーン以外で利用する場合はどうすれば良いでしょうか。
navigation-service.ts
import React from 'react'; export const isMountedRef = React.createRef(); export const navigationRef = React.createRef(); const navigate = (name: string, params?: any) => { if (isMountedRef.current && navigationRef.current) { navigationRef.current?.navigate(name, params); } else { console.log('Not mounted.'); } }; export default { navigate, };App.tsx
const App: React.FunctionComponent = () => { useEffect(() => { // TypeScript error が出る isMountedRef.current = true; return () => (isMountedRef.current = false); }, []); return ( <NavigationContainer ref={navigationRef as any}> <HomeNavigator /> </NavigationContainer> ); };公式ドキュメントでも紹介されている方法ですが、 isMountedRef.current の更新はうまくできないようです。
GitHub にも issue がありました。コンポーネントがマウントされていれば
navigationRef.current?.navigate(...);
は動作します。これで自前の関数から、
const myFunc = () => { NavigationService.navigate('...'); }といった形で利用可能になります。
おわりに
別プロジェクトでは 4.x からバージョンアップを試みており、 TypeScript 周りでかなり苦労しています。
ただし、 Hooks との相性も良さそうですし、新規でプロジェクトを始める場合は 5.x を使ってみても良いかもしれません。型定義周りはもう少し調査が必要そうです。
記事の内容に誤りなどございましたらご指摘いただけると幸いです。
- 投稿日:2020-03-05T08:49:15+09:00
[React]別々に useState で定義した相互依存する state は期待通り動かない
React の useState をいろいろいじってるときに微妙にハマったのでメモ。
useState を複数使って state を定義しているときに、それらが相互依存しているとき期待通りに動かなかった。
なので、相互依存しているものは同じ state に入れることで解消しました。
サンプル
loading
がtrue
のとき、submit
できないようにしたいとする。
(loading === true
のとき、canSubmit === false
にしたい)import React, { FC, useState, useEffect } from 'react'; const StateDependOnEachOther: FC = () => { // 別々の state に定義したバージョン const [canSubmit, setCanSubmit] = useState(true); const [loading, setLoading] = useState(false); // loading の状態に合わせて canSubmit を変える const submitting = () => { setCanSubmit(() => { if (loading) { return false; } return true; }); }; // 同じ state に定義したバージョン const [state, setState] = useState({ canSubmit2: true, loading2: false, }); // loading の状態に合わせて canSubmit を変える const submitting2 = () => { setState(prevCanSubmit2 => { if (prevCanSubmit2.loading2) { return { ...prevCanSubmit2, canSubmit2: false, }; } return { ...prevCanSubmit2, canSubmit2: true, }; }); }; useEffect(() => { // loading を true に変更 setLoading(true); setState(prevState => { return { ...prevState, loading2: true }; }); // loading === true のとき、 canSumit は false にしたい submitting(); submitting2(); // eslint-disable-next-line }, []); return ( <> <div>別々の useState で定義: canSubmit: {`${canSubmit}`}</div> <div>同じ useState で定義 state.canSubmit2: {`${state.canSubmit2}`}</div> </> ); }; export default StateDependOnEachOther;結果
- 別々の useState で定義した場合は loading の変更が反映されず、 canSubmit は true のまま
- 同じ useState 定義した場合は loading の変更が反映され、 canSubmit は false になる
実際に動かしたものがこちら
とりあえず useState に定義すればいいと思ってたけど、関連するものはまとめる、または1コンポーネントの中で useState は一つにして、必要なものは全部その中に突っ込む方が良さそう。
- 投稿日:2020-03-05T03:51:44+09:00
React Native vs. Ionic – A head-to-head Comparison in 2020
In this article, we are going to look at an intense comparison between Ionic and React Native development frameworks. At the end of this article, you will be all clear about what framework you should use for your development projects.
click here to read morehttps://www.positronx.io/react-native-vs-ionic-head-to-head-comparison/
- 投稿日:2020-03-05T03:48:11+09:00
React Native StackNavigator – Passing & Getting Params to Screen
In this tutorial, we are going to learn how to easily pass some value from one screen to another screen using React Navigation’s StackNavigator service.
click here to read more
https://www.positronx.io/react-native-stack-navigator-passing-getting-params-to-screen/
- 投稿日:2020-03-05T03:46:46+09:00
React Native Navigation v5 Example Tutorial
This is a step by step comprehensive React Native Navigation v5 tutorial. In this tutorial, we will learn how to implement Stack Navigation in React Native app using Stack Navigator.
click here to read more
https://www.positronx.io/react-native-navigation-example-tutorial/
- 投稿日:2020-03-05T02:16:24+09:00
React Contextを使う
概要
あるコンポーネントから、子、孫のコンポーネントに値を渡すのに
通常だとpropsを使って順番に橋渡ししていくので手間ですが、
Contextを使うことでそんな面倒はなしにグローバルに値にアクセスできます。
アプリ全体で使う設定をもたせるのに便利だと思います。
Reduxが嫌いな人におすすめです。下記サンプルではParentView -> MiddleView -> ChildViewの階層になっています。
Contextを作成する
const SettingContext = React.createContext({ xxx, yyy });コンポーネントの外に定義しておきます。
引数はデフォルト値になります。
親をたどってProviderがなかった場合に適用されるようです。(そんな事あるのか?)値をセットする
<SettingContext.Provider value={ xxxx }> <MiddleView setting={this.state} /> </SettingContext.Provider>作成したContextのProviderをタグに使ってvalueに値を入れます。
値をゲットする
やり方は2通りあるようです。
- contextTypeを設定する クラスのcontextTypeプロパティにコンテキストを設定します。 そしてthis.contextで値を取り出します。
class ChildView1 extends React.Component { render() { return ( <div> <h3>Passed by contextType</h3> <p>{this.context.color}</p> <p>{this.context.size}</p> </div> ); } } ChildView1.contextType = SettingContext;
- Consumerを使う 作成したContextのConsumerをタグに使います。 中で関数の引数に値が入るのでそれを使います。
class ChildView2 extends React.Component { render() { return ( <div> <SettingContext.Consumer> {context => { return ( <React.Fragment> <h3>Passed by Consumer</h3> <p>{context.color}</p> <p>{context.size}</p> </React.Fragment> ); }} </SettingContext.Consumer> </div> ); } }子のコンポーネントから値を更新する
メソッドを作成してProviderに渡して値と同じように使います。
this.updateState = () => { console.log('update'); this.setState({ color: this.state.color + '!', size: this.state.size + 1 }); };サンプルコード
import React from 'react'; // Contextを定義 const SettingContext = React.createContext({ // default values color: 'red', size: 40, updateState: () => {}, resetState: () => {} }); // Context使わない場合 class ChildView extends React.Component { render() { return ( <div> <h3>Passed by props</h3> <p>{this.props.setting.color}</p> <p>{this.props.setting.size}</p> </div> ); } } // contextTypeを追加 class ChildView1 extends React.Component { render() { return ( <div> <h3>Passed by contextType</h3> <p>{this.context.color}</p> <p>{this.context.size}</p> <button onClick={this.context.updateState}>update</button> </div> ); } } ChildView1.contextType = SettingContext; // Consumerを追加 class ChildView2 extends React.Component { render() { return ( <div> <SettingContext.Consumer> {context => { return ( <React.Fragment> <h3>Passed by Consumer</h3> <p>{context.color}</p> <p>{context.size}</p> <button onClick={context.updateState}>update</button> </React.Fragment> ); }} </SettingContext.Consumer> </div> ); } } class MiddleView extends React.Component { render() { return ( <div> <h2>Parent</h2> <ChildView setting={this.props.setting} /> <ChildView1 /> <ChildView2 /> </div> ); } } class ParentView extends React.Component { constructor(props) { super(props); this.updateState = () => { console.log('update'); this.setState({ color: this.state.color + '!', size: this.state.size + 1 }); }; this.state = { color: 'green', size: 50, updateState: this.updateState }; } render() { return ( <div className="App"> <SettingContext.Provider value={this.state}> <MiddleView setting={this.state} /> </SettingContext.Provider> <div> <h2>default</h2> <ChildView1 /> <ChildView2 /> </div> </div> ); } } export default ParentView;まとめ
contextTypeの方がスッキリして見えるがクラス外にコードを書くのが嫌。
単純なものならReduxを使うよりシンプルというのは言えるかな。
初心者の私にはわかりませんが。