どうも、初めての方は初めまして、そうじゃない方はお久しぶりです、ヒロです!
この記事は SmartDrive Advent Calendar 2018 - Qiita の9日目です。
今回は 16.7.0-alpha.0
で追加されたhooks APIを使用して react-redux
に似たような実装のサンプルを作成したのでご紹介します!
後一瞬だけ告知させてください、来たる 2018年12月30日(日)、コミックマーケット95 2日目(日) 東ト38b
にてこれから始める ♡React.js 実践マニュアル
という新刊出します。
過去本もいくつか持参するのでもし良ければ下記のページを見てみてください。
過去本一覧ページ
そして今回する紹介する内容はその新刊で紹介する予定だったけど、紙面オーバーして断念してしまった内容になります。汗
Reach The Sky, Without Redux
今回はHacker NewsのAPI を使用して、簡易的なHacker Newsクライアントを作成しました。
下記がサンプルの画面になります。
こちらのリポジトリ から手元にクローンしてから yarn start:chapter2
をターミナルで叩く事で上記の画面を確認出来ます。
基本的な構成
リポジトリを見てもらうとわかりますが、基本的な構成は下記のようになっています。
./chapter2
├── index.html
└── src
├── Context.jsx
├── Provider.jsx
├── actions
│ └── index.js
├── components
│ ├── Header
│ │ ├── index.jsx
│ │ └── style.js
│ ├── List
│ │ ├── index.jsx
│ │ └── style.js
│ ├── ListArea
│ │ ├── index.jsx
│ │ └── style.js
│ └── index.js
├── connect.js
├── containers
│ └── App
│ └── index.js
├── index.js
├── modules
│ ├── fetchLogic.js
│ ├── getFavoItems.js
│ ├── getSearch.js
│ └── useWindowWidth.js
├── pages
│ └── MainView
│ └── index.jsx
├── reducers
│ ├── combineReducers.js
│ ├── favorite.js
│ ├── index.js
│ └── posts.js
└── store.js
一つ一つ解説して行きます。
Context.jsxについて
Context.jsx
はContextを作成しているファイルになります。hooksを使用すると従来のConsumerの記述よりも少し記述方法が楽になります。
import { useContext } from 'react';
import Context from './Context';
const store = () => {
const { store, dispatch } = useContext(Context);
return {
getState: store,
dispatch
};
};
export default store;
またさらっとstore内のコードを紹介してしまいましたが、 store.js
ファイル内ではContextを読み込んで getState
と dispatch
を返しています。
Provider.jsxについて
react-redux
で提供されているProviderのような役割を担っています。
import React, { useState, useReducer, useEffect } from 'react';
import Context from './Context';
type Props = {
children: any,
reducer: () => {}
};
const Provider = ({ children, reducer }: Props) => {
const [store, dispatch] = useReducer(reducer);
const [state, setState] = useState({ isLoaded: false });
useEffect(() => {
dispatch({ type: '@init' });
setState({ isLoaded: true });
}, []);
return (
<Context.Provider value={{ dispatch, store }}>
{state.isLoaded ? children : false}
</Context.Provider>
);
};
export default Provider;
Provider
コンポーネントでは props
でchildren
と reducer
を受け取っています。 provider
コンポーネントがレンダリングされたら useEffect
経由で dispatch({ type: '@init' });
を発火して、 reducer
内で返しているオブジェクト(initialStore)をstoreに反映します。反映が完了したら setState
で isLoaded
を true
にし、 children
をレンダリングします。
reducersについて
reducers
には主に2つの役割があります。
- storeに反映させる為のreducer
- reducerを束ね上げるcombineReducers
storeに反映させる為のreducerとは通常のreducerになります。今回のサンプルの中で例を上げると posts.js
などですね。
const init = {
data: []
};
const posts = (state = init, action) => {
if (action.type === 'ADD_POST') {
return { ...state, data: action.payload };
}
return state;
};
export default posts;
また combineReducers
については reducer
を一つの reducer
にまとめる役割を持ちます。
const combineReducers = reducer => {
return (state = {}, action) => {
const keys = Object.keys(reducer);
const nextReducers = {};
for (let i = 0; i < keys.length; i++) {
const invoke = reducer[keys[i]](state[keys[i]], action);
nextReducers[keys[i]] = invoke;
}
return nextReducers;
};
};
export default combineReducers;
actionが発火されるとreducer内の全てのケースが発火されて該当するreducerには該当したreducer内の処理が行われ、それ以外は state内に保持してある値がそれぞれ割り振られているので、割り振った値がそのまま返されます。
connect.jsについて
connect.js
についてはconnectを React Component
に適用した際に mapStateToProps
と mapDispatchToProps
を適用させる為の処理が記述されています。
import React, { useContext } from 'react';
import Context from './Context';
const connect = (mapState, mapDispatch) => {
return WrappedComponent => {
return () => {
const { store, dispatch } = useContext(Context);
return (
<WrappedComponent {...mapState(store)} {...mapDispatch(dispatch)} />
);
};
};
};
export default connect;
特に難しい事はしてなく、至ってシンプルなHOCになっています。
actionsについて
actions
は action
の記述がされています。
export const addTodo = (payload: Array<Object>) => ({
type: 'ADD_POST',
payload
});
export const addFavoItem = (payload: number) => ({
type: 'ADD_FAVORITE_ITEM',
payload: {
id: payload
}
});
componentsにとpagesについて
これはUI用のコンポーネントが記述されています。通常の React Component
が記述されている為、ここでは割愛します。ちなみに pages
は各page毎の statelessなコンポーネントを置く場所、という立ち位置ですが、今回の場合は1ページしかないので、あんまり分ける意味はなかったです。が、念の為補足します。
modules
ここには非同期周りの処理が記述されています。本当はredux-sagaみたいな事をする、もしくはredux-sagaと互換性を持たせたかったんですが、時間の都合上断念し、非同期周りの記述を modules
配下に設置しています。
containersについて
containers
内の App
内で下記のような記述をしています。
import React, { useEffect, useCallback } from 'react';
import MainView from '@chapter2/pages/MainView';
import useWindowWidth from '@chapter2/modules/useWindowWidth';
import { getSearch, getFavoItems } from '@chapter2/modules/fetchLogic';
import { addTodo, addFavoItem } from '@chapter2/actions';
import connect from '../../connect';
const App = ({ posts, addTodo, addFavoItem, favorite }) => {
const windowWidth = useWindowWidth();
const invokeSearch = async ({ searchValue, keyCode, type = 'default' }) => {
if (keyCode === 13) {
const search = await getSearch(searchValue);
addTodo(search.hits);
return;
}
if (type === 'submit') {
const search = await getSearch(searchValue);
addTodo(search.hits);
return;
}
};
const invokeFavoItems = async () => {
const getItems = await getFavoItems(favorite);
const addingFavoFlag = await getItems.map(item => ({
...item,
isFavorite: true
}));
await addTodo(addingFavoFlag);
};
const memoizePosts = useCallback(
() => {
const mapItem = posts.map(item => {
const checkFavo = favorite.includes(
item.id ? item.id : Number(item.objectID)
);
return {
...item,
isFavorite: checkFavo
};
});
return mapItem;
},
[posts, favorite]
);
useEffect(() => {
(async () => {
try {
const data = await getSearch();
addTodo(data.hits);
} catch (e) {
// error logic
}
})();
}, []);
const hundlers = {
invokeSearch,
posts: memoizePosts(),
windowWidth,
addFavoItem,
invokeFavoItems
};
return <MainView {...hundlers} />;
};
const mapStateToProps = store => ({
posts: store.posts.data,
favorite: store.favorite.favorite_posts
});
const mapDispathToProps = dispatch => ({
addTodo: param => dispatch(addTodo(param)),
addFavoItem: param => dispatch(addFavoItem(param))
});
export default connect(
mapStateToProps,
mapDispathToProps
)(App);
整理されていない部分も多いですが、まず App
コンポーネントの props
に mapStateToProps
と mapDispathToProps
でpropsに埋め込む指定をした props
を指定します。 invokeSearch
と invokeFavoItems
検索した際とお気に入りした記事を一覧で表示させる際に非同期でリクエストしてデータを取得する為の記述です。 memoizePosts
については useCallback
を使用して、memoizationをしています。 また useEffect
内で初期のデータをリクエストして取得し、 addTodo
action経由でstoreに反映させます。
最終的に子コンポーネントpropsを渡す為に hundlers
というオブジェクトに渡すpropsをまとめ、 MainView
コンポーネントにpropsを渡しています。
最後に connect
する事によって完了です。
最後に
如何でしたでしょうか?かなりざっくりした形で紹介してしまいましたが、hooks APIを使用する事で簡易的なreduxのような実装が可能になりました。まだalphaリリースの段階ですが、これが正式にリリースされたら React Class Component
を記述する回数が今までよりも減りそうな気がします。
smartdriveではReactが大好きなフロントエンドエンジニアを募集しています!