React・Reduxで多くの入門記事には書かれていないけど重要なこと

チュートリアル的なページに意外と載っていないことを書いてみる。
特にReduxのあたりの内容が理解できれば初心者の域は脱出していると思われる。

子ノードの受け取り方

const Container = props => <div className={'container'}>{props.children}</div>;

変数で属性値を示す

const attributes = {alt: 'テスト'};
const Container = () => <div {...attributes}></div>;

配列のテキストを連結して表示

よくArray.mapで展開する例を見かけるが、Reactでは配列の中の要素は繰り返し出力されるため、単純なテキストの連結はこれでいい。

const array = ['hoge1', 'hoge2', 'hoge3'];
const Container = () => <div>{array}</div>; // hoge1hoge2hoge2

React.Fragmentの省略形

// 下記は一緒
const ComponentA = () => (
  <React.Fragment>hoge</React.Fragment>
);

const ComponentB = () => (
  <>hoge</>
);

同じ内容のAction(Creator)ファイルを名前空間を利用して分割

下記のように単純にコピーしただけでもSymbolによって名前空間が利用できる。

// hoge1.js
export const ACTION_A = Symbol('ACTION_A');
export const hoge() {
  return {type: ACTION_A};
};
// hoge2.js
export const ACTION_A = Symbol('ACTION_A');
export const hoge() {
  return {type: ACTION_A};
};

Stateはimmutable(不変)

下記のようにStateを変更していることがある。

const reducer(state = initialState, action) => {
  if (action.type === 'Action') {
    state.hoge = action.payload.hoge;
    return state;
  }
};

これではimmutableの原則に則っておらず、Reactが値の変更が感知できない場合があるため、次のように記述する。

const reducer(state = initialState, action) => {
  if (action.type === 'Action') {
    return {
      ...state,
      hoge: action.payload.hoge,
    };
  }
};

const reducer(state = initialState, action) => {
  if (action.type === 'Action') {
    const newHoge = action.payload.hoge.slice();
    return newHoge;
  }
};

あまりReduxに縛られない

Reduxを使っていると、すべての状態をStoreで管理したくなってしまうが、そうすると無駄にActionやReducerが肥大化してしまうため、コンポーネント内部で完結する状態については、useStateを存分に活用したほうが、結果的に柔軟な設計になりやすい(経験則)。

Reducerの分割の原則

忘れがちだが、Reduxの公式ドキュメントでは、レンダリングツリー(画面ごとの分割)ではなく、ドメインデータ(役割の範囲、分野)ごとに分割することが推奨されている。
ちなみにStateはDomainState、AppState、UIStateの3つに分割することが提案されている。
DomainStateはドメインデータ、AppStateはアプリケーション全体の設定、UIStateはUIに関連する状態に分割する感じ。

FSA(Flux Standard Action)について

FSAはActionの型の標準化するための規約のこと。
次のような型を使って書くのがルールである。

// standard
{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.' ,
  },
  meta: {
    // payload以外の追加情報
  },
}

// error
{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true,
}

処理の成功・失敗ごとにActionを分割するのではなく、Actionのtypeを同じにしつつ、わかりやすくまとめることができる。

Middlewareの役割

ReduxはComponent、Reducer、ActionCreatorはすべて純粋関数で記述できるため、副作用のある処理はすべてMiddlewareに押し付けて書く。
ほとんどの場合、非同期処理でMiddlewareにお世話になる。

個人的にはMiddlewareはあまり使わないので、最後によくやるコンポーネントに非同期処理を書く場合のおおまかなやり方を示す。
フェッチ前とフェッチ後でActionを作る感じ。

// Reducer
const initialState = {
  data: '',
  isFetching: false,
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'REQUEST_DATA':
      return {
        ...state,
        isFetching: true,
      }
    case 'RECEIVE_DATA':
      return {
        ...state,
        data: action.payload.data,
        isFetching: false,
      }
    default:
      return state
  }
};

// ActionCreator
const requestData = () => ({
  type: 'REQUEST_DATA',
});

const receiveData = data => ({
  type: 'RECEIVE_DATA',
  payload: {
    data,
  },
});

// Component
const Component = () => {
  const dispatch = useDispatch();
  const data = useSelector(state => state.data);
  const isFetching = useSelector(state => state.isFetching);
  const clickhandler = async () => {
    dispatch(requestData());
    const newData = await someAPI();
    dispatch(receiveData(newData));
  };

  return (
    <>
      <p>{isFetching ? '...' : data}</p>
      <button onClick={clickHandler}>データを取得</button>
    </>
  );
};

HOC(Higher-Order Components)

高階関数のReact Components版。
高階関数とは呼び出された時に関数を返す関数のことで、関数を引数に受け取ったりする。

// classNameを変更不可に
const HOCComponent1 = (props) => {
return <SourceComponent {...props} className={'hoc'} />;
};

// classNameを変更可能に
const HOCComponent2 = (props) => {
return <SourceComponent className={'hoc'} {...props} />;
};

// propsによって表示するコンポーネントを変更
const HOCComponent3 = (props) => {
if (props.pattern === 'A') {
return <SourceComponentA />;
} else {
return <SourceComponentB />;
}
};

// コンポーネントを引数に
const HOCComponent3 = (Component) => {
const data = {
message: 'hoge',
};
return (props) => {
return <Component {...data} {...props} />
};
};

駆け出しWebデザイナーに向けて。UI・UXを考察するにあたってのアドバイス

自分なりにまとめてみた。けっこう抽象的な内容。

自然な流れにする

まず自然とはなにかということを考えると

  • 流行のUI・UXと同じ(あるいは模した)構造になっているか
  • アニメーションイージングなどで自然現象(摩擦や反動など)が考慮されているか
  • 文脈が正しいかどうか(突拍子のないプロセスとなっていないか)
  • サイト全体の文脈に逆らっていないか(同じUIで実現可能なのに例外を作っていないか。UIが統一されているかどうか)

など一般論や自然現象が成立しているかどうかである。
すべて重要な要素だが、特にアニメーションイージングの自然現象の考慮は後回しになりがちである。
マイクロインタラクションの領域でもあるのだが、僕が特に意識しているのがヘッダーメニューやフッターメニュー。
コーポレートサイトの場合にはすべてのページで同じヘッダーフッターを採用することになると思うのだが、サイト回遊でメインで利用されるリンクが集まっており、一番ユーザーが操作する領域である。
そのため、個人的にはヘッダーフッターでサイトの使いやすさの5割が決まるといっても過言でないと思っており、ヘッダーフッターメニューが快適に操作できないサイトはUI・UXが優れていないと断定できると思う。

考えるべきことを減らす

システム設計についても言えることだが、複雑な方程式は好かれない。
理解や操作が容易であることが大前提である。
具体的な判断材料は下記。

  • ユーザーの選択肢を(機能性を失わず可能な限り)少なくすること
  • 目標に至るまでのユーザーの操作手順を少なくすること
  • 操作手順を言語で簡単に示せること(言葉で説明しにくいものはUIが難しいことと同義)

スピードは命

UI反応速度、ページの表示速度はUXに直結する。
エンジニアの力量ではカバーできない領域もあり、デザイナー自身でも意識しなければいけない。
具体的な意識としては、

  • レイアウトを頑張らない(複雑なレイアウト、多種のレイアウトを作らず、設置する画像に注力する)
  • コンポーネントの種類・サイズを限定する(シンプルなコンポーネントの組み合わせでデザインする。Atomic Designを意識)

両者ともに言えるが、要するにシンプルなデザインにするということだ。
シンプルにすることによって、いくつかのメリットがある。

  1. CSSの軽量化・最適化(=表示速度の向上)
  2. 保守性の向上
  3. CSSコーディング時間の削減
  4. CMSの場合、エディトリアルの単純化(テンプレートファイルの削減)
  5. サイト全体のデザインの統一性の担保

特に2と3が大きい。
保守性が高くなることで、改修などでのCSSファイルの肥大化を減少させてくれる。
またフロントエンドエンジニアのリソースが少ない組織の場合、今のフロントエンドはJavaScriptに注力すべきであり、CSSコーディングになかなか力が入れられないため、この手間が軽減されることでエンジニアの評価はだだ上がりすること間違いなしである。

シンプルなデザインでも、よくあるスクロールに応じたアニメーションなどをつけたりなどすることで、複雑なレイアウトよりも効果的だったりするので、余った時間をそちらのリソースに割く方針のほうがなにかと都合がいいと思う。

適正なサイズ感覚を身につける

かっこ良さを追求するとコンポーネントサイズやフォントサイズを小さくするデザイナーがいたりするが、そういったことはUIにとって大抵悪影響である。
実際のブラウザ表示サイズで確認し、半目にして見えづらい状況で確認したり、遠くからみてみたりして、視認・操作がしやすいサイズかを検証してほしい。

余談だがデザイン学部時代、英語での表記を多様する学生がいて、見兼ねた先生がそいつに英語を使わずデザインするよう指示していた。
どうしようもなくデザインしようがない場合には、そういった小手先のテクに走ってしまうのも仕方がないが、それほど切羽詰まっていない場合には控えてもらうようデザイナーに伝えたいのがエンジニアとしての本音である。

ジェネレーターについて

個人的にReduxの非同期処理は基本はMiddlewareを使わずコンポーネントに処理を書くことが多いが、redux-sagaでなんとなく使っていたジェネレーターについて復習がてら筆を執った次第。

function* generator() {
  yield 'A';
  yield 'B';
  yield 'C';
  yield* ['D', 'E', 'F'];
  let test1 = yield 'G';
  let test2 = yield 'H';
  yield* [test1, test2];
}

const gen = generator();

console.log(gen.next()); // { value: 'A', done: false }
console.log(gen.next()); // { value: 'B', done: false }
console.log(gen.next()); // { value: 'C', done: false }
console.log(gen.next()); // { value: 'D', done: false }
console.log(gen.next()); // { value: 'E', done: false }
console.log(gen.next()); // { value: 'F', done: false }
console.log(gen.next()); // { value: 'G', done: false }
console.log(gen.next('I')); // { value: 'H', done: false }
console.log(gen.next('J')); // { value: 'I', done: false }
console.log(gen.next()); // { value: 'J', done: false }
console.log(gen.next()); // { value: undefined, done: true }
function* generator(count) {
  while (1) {
    yield count++;
  }
}

const gen = generator(0);
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }

イテレーター、あるいは途中で止まる関数として非同期処理を順序良く実行するのに使用機会がある。

しょぼいエンジニアの特徴をひたすら挙げていく

常識がない

別業種の人からみるとエンジニアの作業はブラックボックス化されているが、実際はプログラムという言語を使って、機械が理解するためにわかりやすく、順序立てて説明することに等しい。
つまり第一言語でうまく説明したり常識的な回答を見い出せる人は、プログラミングも必ずうまくやれる(これが一般的なベンダーで文系プログラマーのほうが活躍できると謳っている理由)。
逆に言えば、常識がない人はプログラミングがうまくない。

ブラインドタッチができない

ブラインドタッチができないのは、プログラムを書いている時間が短いからに他ならない。
Qiitaで知識だけ得て手を動かさない頭でっかちが現場でできることはといえば、抽象的な仕様書を書き曖昧な指示を送ることだけだ。
口だけが達者なエンジニアは新しい技術情報を仕入れる際に、そのツールの名前や使い方を単純に覚えるだけだが、本当に必要なのはそういった選択肢が存在するという事実を理解し、その背景を把握することである。

特定の技術に固執している

新しい技術に積極的ではないとも言える。
現在自分ができる範囲から飛び出したものを学ぶ姿勢がなければ成長が難しい。
ストックがないから、プログラム上で個人の「こだわりの独自ルール」を適用してしまいがちになり、他人からはブラックボックスとなってしまう。

少し話がそれるが、2019年現在、Web系エンジニアが下記のいずれかで深い知識がないと優秀とはいえないと思う。

  • 機械学習
  • AWS(をすぐに使いこなせるようになるほどのインフラ知識)
  • ReactなどのいわゆるMVVMフレームワークとReduxなど状態管理フレームワークを使った開発
  • WebGLのシェーダー

それ以外のWAI-ARIAだったりレスポンシブ用のCSSコーディング、バンドラーの知識やWordPressなどは、結局のところ突き詰めて開発を進めていくと上記項目のいずれかを習得する必要がある。
機械学習とAWSはバックエンド向けだが、Reactやシェーダーに詳しいフロントエンドエンジニアがHTML・CSSなどの基礎技術が不自由であるとは考えがたい。
またAWSと書いたのは、バックエンドにマイクロサービス化(APIサーバー化)が求められる中で確実に一度は通る道だからだ。

本質を話さない(会話・文章内容が曖昧)

(エンジニアに限ったことではないが)本質の理解ができていないと適切なリファクタリングができない。
例えば、仕様についてディレクターなどプログラムに精通していない人にできる・できないの判断を仰がれるときに、自分の軸で話しているか、あるいは技術的なレベルでするか・しないかというケースに分けて会話しているかという点に着目すると、技術レベルがよくわかる(時間がなくイエスノーのみで答えてしまう場合もあるとは思うが)。

自分の書いたコードに責任を持っていない

Webで調べたものを動作検証などをせずコピペで済ませたり、引き継ぎの際に明確なドキュメントあるいは十分な言葉を渡さないなど、そのコードに責任を持っていない人は仕事にドライといえるだけでなく、作る際に何も考えていなかったために単純に思い出すのが困難だからである。
情熱を持ったエンジニアであれば、それを面倒とは思わず積極的に関わるであろう。
「これオレが作ったんだぜ!」って自慢できるようなプロダクトを毎回作れば自然とコードに責任が持てるようになる。


…と偉そうに書き連ねてみたが、他人から見たら自分はどうなんだっていう。。

ReactのsetStateの注意点

将来的な非同期レンダリングのため、setState内ではstateを読み込まない。

その代わりsetStateの第一引数に関数を指定するとその関数内で現在の値を取得できるため、それを利用する。

// stateがリテラルの場合
import { useState } from 'react';
const [stateCount, setStateCount] = useState(0);

<button type="button" onClick={() => setStateCount(beforeCount => beforeCount++)}>インクリメント</button>
// stateがオブジェクトの場合は関数でないとキーの一部を変更する処理ができない
import { useState } from 'react';
const [stateItem, setStateItem] = React.useState({title: 'hoge', text: 'hoge'});

const onChangeText = e => {
  const text = e.target.value;
  setStateItem(oldState => ({...oldState, text}));
}

<input type="text" value={stateItem.text} onChange={ onChangeText } />

Netlify Functionsのテスト方法

Netlify FunctionsとはNetlify版のいわゆるFaaS(ファース・Function as a Service)。

Netlify Functionsの使い方としてはnetlify.tomlという設定ファイルでNetlify Functionsの関数を置く場所を指定しファイルを置くとエンドポイントが自動で付与されるので、それを使用する感じ。

テストはnpmでnetlify-lambdaというパッケージが配布されているため、それを用いる。

$ npm init -f
$ npm install netlify-lambda

次に設定ファイルのnetlify.tomlを用意する。

$ touch netlify.toml
$ vi netlify.toml

[build]
	functions = "functions"

上記の設定により、functionsディレクトリ内にNetlify Functionsのファイルを置けるようになった。

次にsrcディレクトリ内に関数ファイルを設置する。

$ mkdir src
$ touch src/test.js
$ vi src/test.js

exports.handler = (event, context, callback) => {
  callback(null, {
    statusCode: 200,
    body: 'Hello World!!',
  })
};

そして下記のコマンドでテストしてみる。

$ npx netlify-lambda serve ./src/

そうするとコンソールに下記のような感じでポート番号が書かれるのでブラウザでアクセスしてみる(下記の例ではhttp://localhost:9000)。

Lambda server is listening on 9000

問題ないようであれば、package.jsonに下記のbuildコマンドを記述し、netlify.tomlにそのbuildコマンドを実行するように指定する。

$ vi package.json

~ 略
"scripts": {
  "build": "netlify-lambda build ./src/"
},
~ 略
$ vi netlify.toml

[build]
	functions = "functions"
	command = "npm run build"

functionsディレクトリはgit管理しないようにし、pushすると、自動ビルドされるようになる。
これで一旦ひと通りの流れは完了となる。

Netlify Functionsは内部的にはAWS Lambda(ラムダ)を使っているらしい。

無料版では2019年6月24日現在、月に125,000リクエスト、実行時間は100時間まで利用できるため、個人で使うぶんには問題なく無料で使えそう。
ただしURLはNetlify独自の形式から変更できない、メモリは128MB、実行時間は10秒までとなっている。

GithubでOSS参加するためのコマンド集

(1)Githubのforkボタンをクリック

(2)Cloneする

$ git clone https://github.com/isaxxx/~

(3)開発用ブランチの作成

$ git checkout -b develop
$ git branch
* develop
  master

(4)開発用ブランチで変更を加え、コミット・プッシュ

$ vi README.txt
$ git status
~
 modified: README.txt
~
$ git add README.txt
$ git commit
$ git push origin develop

(5)Branchをmasterに変更し、Fork元リポジトリをローカルで管理する

$ git checkout master
$ git remote add upstream https://github.com/~/~
$ git fetch upstream
$ git branch -a
  remotes/origin/HEAD -> origin/master
  remotes/origin/develop
  remotes/origin/master
  remotes/origin/v1
  remotes/origin/v2
  remotes/upstream/master
  remotes/upstream/v1
  remotes/upstream/v2

(6)Fork元リポジトリの最新バージョンを取得し、ローカルのmasterを更新・pushする

$ git fetch upstream
$ git merge upstream/master
$ git push origin master

(7)ローカルのmasterと開発用ブランチをマージする

$ git merge develop

(8)Github上でPull Requestを送る

WordPressセキュリティ対策

wp-cron.phpを無効化

/* wp-config.php に追記 */
define('DISABLE_WP_CRON', 'true');

※wp-cron.phpを無効化した場合、予約投稿や通知機能などが動作しなくなるため、サーバーcronを利用

$ sudo vi /etc/crontab
# 1分ごとにcronを実行
* * * * * apache /usr/bin/php ~/wp-cron.php > /dev/null 2>&1

xmlrpc.phpのアクセス制限

# vi ~/.htaccess

<Files xmlrpc.php>
Order deny,allow
Deny from all
</Files>

wp-login.phpをアクセス制限

# vi ~/.htaccess

<Files wp-login.php>
Order deny,allow
Deny from all
Allow from xxxx.xxxx.xxxx.xxxx
Allow from xxxx.xxxx.xxxx.xxxx
...
</Files>

wp-login.phpにBasic認証を設置

# vi ~/.htaccess

<Files wp-login.php>
AuthType Basic
AuthUserFile ~/.htpasswd
AuthName "Please enter your ID and password"
require valid-user
</Files>

wp-comments-post.phpのアクセス制限

# vi ~/.htaccess

<Files wp-comments-post.php>
Order deny,allow
Deny from all
</Files>

wp-trackback.phpのアクセス制限

# vi ~/.htaccess

<Files wp-trackback.php>
Order deny,allow
Deny from all
</Files>

複数設定例

# vi ~/.htaccess

<Files ~ "^(wp-config|wp-cron|xmlrpc)\.php$">
Order deny,allow
Deny from all
</Files>

wp-adminディレクトリ以下(admin-ajax.phpを除く)をアクセス制限

# vi ~/wp-admin/.htaccess

Order deny,allow
Deny from all
Allow from xxxx.xxxx.xxxx.xxxx
Allow from xxxx.xxxx.xxxx.xxxx
...
<Files admin-ajax.php>
Satisfy Any
Order allow,deny
Allow from all
Deny from none
</Files>

wp-adminディレクトリ以下(admin-ajax.phpを除く)にBasic認証を設置

# vi ~/wp-admin/.htaccess

AuthType Basic
AuthUserFile ~/.htpasswd
AuthName "Please enter your ID and password"
Require valid-user
<Files admin-ajax.php>
Satisfy Any
Order allow,deny
Allow from all
Deny from none
</Files>

wp-config.phpのアクセス制限

# vi ~/.htaccess

<Files wp-config.php>
Order deny,allow
Deny from all
</Files>