チュートリアル的なページに意外と載っていないことを書いてみる。
特に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} />
};
};
属性値
属性値はブール値で出力の有無を設定可能。
// disabled属性が設定される
<button disabled={true} />
// disabled属性が設定されない
<button disabled={false} />
useEffectでcomponentDidMountを代用する方法
useEffectは第二引数になにも代入しない場合は、rendarが呼ばれるたびに実行される。
そのため、第二引数に配列で発火させる条件の変数を入れる必要がある。
export default function Home({ allPostsData }) {
const componentDidMount = 1; // 初回のみの発火するため定数宣言
useEffect(() => {
console.log('componentDidMount');
}, [componentDidMount]);
// 略
}
useEffectでcomponentWillUnmountを代用する方法
useEffectで関数を返してあげると、アンマウント時にその関数が実行される。
export default function Home({ allPostsData }) {
useEffect(() => {
console.log('componentDidMount');
return () => {
console.log('componentWillUnmount');
};
});
// 略
}
useEffectの関心の分離
useEffectは何度でも記述することができるため(そもそもReactHooksが導入されたきっかけは、それぞれのライフサイクルメソッドが単独であるため、関連のないロジックが追加されてしまうことにあった)、それぞれの関連に関しては(当たり前だが)別のuseEffectを使用するべき。
useEffect での Race Condition と debounce
// ローディングの状態管理
const [loading, setLoading] = useState(false);
const [hoge, setHoge] = useState('');
useEffect(() => {
setLoading(true);
getHogeData().then(data => {
setHoge(data);
setLoading(false);
}
});
}, [hoge]);
下記のような場合、 getHogeData が呼び出した順番に結果が返ってくるとは限らないため、最後に返ってきた値を反映する必要がある。
// ローディングの状態管理
const [loading, setLoading] = useState(false);
const [hoge, setHoge] = useState('');
useEffect(() => {
let cancel = false;
setLoading(true);
getHogeData().then(data => {
if (!cancel) {
setHoge(data);
setLoading(false);
}
}
});
return () => {
// 先にDOM更新されていたらキャンセルフラグが立つ(スコープはこの関数内なので、この関数のみがキャンセルされる)
cancel = true;
}
}, [hoge]);
debounceの場合は下記。
// ローディングの状態管理
const [loading, setLoading] = useState(false);
const [hoge, setHoge] = useState('');
const debounceTime = 1000;
useEffect(() => {
setLoading(true);
const timer = setTimeout(() => {
getHogeData().then(data => {
if (!cancel) {
setHoge(data);
setLoading(false);
}
});
}, debounceTime);
return () => {
clearTimeout(timer);
}
}, [hoge]);
開発時、コンポーネント管理する際、ディレクトリ分けしすぎない
MVVMフレームワークには Atomic Design など多くの誘惑があるが、どのディレクトリに分割するかがコストになりやすく、たいていの場合ディレクトリ分割しすぎると検索時に困ってしまう。
結局ひとつのディレクトリにすべてのコンポーネントをまとめていれたほうが、わかりやすいことが多い。
その際、コンポーネント名は長くなってもいいから(当たり前だが)命名規則は厳格化する。
React.Fragmentもkey属性が使える
{ List.map( (item, index) => ( <React.Fragment key="{index}">
<dt>{ item.dt }</dt>
<dd>{ item.dd }</dd>
</React.Fragment> )) }