20210908のReactに関する記事は11件です。

ApolloClient × Custom HookでGrahQLサーバのデータを取得する

はじめに GrahQLをサポートするApolloClientと、React CustomHookを使って、サーバ上のデータを取得する方法を紹介します。 サンプルイメージ 今回はバックエンドGrahQLサーバとして、SpaceXが提供しているGrahQL APIを利用します。 前提 create-react-app等で、既にReactプロジェクトが作成されていること。 手順 ライブラリインストール AppolloClientの設定 useQueryでデータ取得 ライブラリインストール 下記を実行し、ApolloClientをインストールする。 npm install @apollo/client graphql AppolloClientの設定 上位のコンポーネント(index.jsとか)で必要なオブジェクトをimportする。 index.js import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; ApolloClientをSpaceXのGraphQLサーバと接続するように設定し、インスタンス化する。 (ここで指定するcacheは、ApolloClientを通じてやり取りしたデータのキャッシュを保持するオブジェクトのこと。cache内のデータはApolloProviderにラップされているコンポーネントならどこからでも参照できます。) index.js const client = new ApolloClient({ uri: 'https://api.spacex.land/graphql/', cache: new InMemoryCache() }); その後、下記のようにGraphQLサーバと接続するコンポーネントをApolloProviderでラップしてあげる。(ApolloProvider配下にいる場合のみ、引数として受け渡されたclientを使ってリクエストを投げることができます。) index.js ReactDOM.render( <React.StrictMode> <ApolloProvider client={client}> <App /> </ApolloProvider> </React.StrictMode>, document.getElementById('root') ); ここまでのindex.js全体 index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { ApolloClient, InMemoryCache, ApolloProvider, } from "@apollo/client"; const client = new ApolloClient({ uri: 'https://api.spacex.land/graphql/', cache: new InMemoryCache() }) ReactDOM.render( <React.StrictMode> <ApolloProvider client={client}> <App /> </ApolloProvider> </React.StrictMode>, document.getElementById('root') ); useQueryでデータ取得 ここから、データ取得のロジックを作成していきます。 下記を実行し、GrahQLサーバへのリクエストクエリ発行に必要なオブジェクトをimportします。 App.js import { useQuery, gql } from "@apollo/client"; クエリ作成 gqlを利用して、クエリを作成します。 下記クエリはSpaceX GrahQL APIから、指定した名前の船情報(船名・画像・id)を取得するクエリです。 App.js const GET_SHIPS = gql` query GetShips() { ships(find: {name: 'American Champion'}) { name image id } } `; クエリのリクエスト 作成したクエリをuseQueryを使って、リクエストします。 useQueryは{ loading, error, data }を返却します。 ロード中であればloading、 エラーが発生すればerror、 正常にデータが取得出来たらdataを返却します。 上記以外にもuseQueryはいろいろ便利なもの返却するので、下記参照してみてください。 https://www.apollographql.com/docs/react/data/queries/#usequery-api App.js const shipName = ''; const { loading, error, data } = useQuery(GET_SHIPS ,{ variables: {name: shipName}, }); if (loading) return 'loading'; if (error) return 'error'; return { shipData:data.ships } 上記ロジックをカスタムフックにまとめたコードが下記になります。 App.js import './App.css'; import { useQuery, gql } from "@apollo/client"; import React from 'react'; function useExchangeRates() { const GET_SHIPS = gql` query GetShips() { ships(find: {name: "American Champion"}) { name image id } } `; const shipName = 'American Champion'; const { loading, error, data } = useQuery(GET_SHIPS); if (loading) return 'loading'; if (error) return 'error'; return { shipData:data.ships } } function ShowshipData(props){ if(!props.shipData){ return(<p>no data</p>); }else if (props.shipData === 'loading'){ return(<p>loading</p>); }else if(props.shipData === 'error'){ return(<p>loading</p>); } return props.shipData.map(({ name, image }) => ( <div> <p>{name}</p> <img src={image} alt=''/> </div> )); } function App() { console.log("App rerendered"); const {shipData} = useExchangeRates(); return ( <div className="App"> <div> <ShowshipData shipData={shipData}/> </div> </div> ); } export default App; 以上が、GrahQLサーバのデータを取得する方法になります。 おまけ リクエストの引数(variables)を変えて、再度データ取得を行う 上記コード例の場合、クエリのvariablesが固定値(American Champion)です。 任意のvariablesを指定かつ、一度実行したクエリを手軽に再実行する方法を紹介します。 クエリ作成 クエリを下記の通り修正します。 GetShipsクエリが、引数としてnameを受け取る宣言をしてます。 App.js function useExchangeRates() { const GET_SHIPS = gql` query GetShips($name: String!) { ships(find: {name: $name}) { name image id } } `; クエリのリクエスト useQueryの第二引数で指定しているvariablesがクエリの引数になります。(今回で言うと船名) App.js const shipName = 'other ship'; const { loading, error, data, refetch } = useQuery(GET_SHIPS ,{ variables: {name: shipName}, }); 上記実行すると、指定した船名のデータを取得することができます。 補足: 上記のようにuseQueryの返り値として、refetchを取得しておけば、次回データ取得の際に下記のように利用することができます。 App.js refetch({ variables: {name: shipName} }) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで配列をマッピングする際に三項演算子を使う。

今Reactを使ってSPAのアプリを作っているのですがググって見つけた記事が間違っておりのでハマったので新しく忘備録のために記事を書いておこうと思います。 APIでデータを取得して配列になっているオブジェクトを取得してmap関数で逐一オブジェクトをマッピングしたい時、配列が空であればnullで非表示にしたい場合。 これ三項演算子 ↓ objects ? objects.map(object)=>(<ここにオブジェクトを一つ一つマッピングたいコンポーネント>):null となっているサイトがあるのですがobjectsが空配列で[]の時これは条件分岐判定でfalseにならないのでobjects.lengthにして三項演算子に判断させる必要があります。[].lengthであれば三項演算子てfalseと判断してくれます。 ちょっと自分のコードを掲載しておきます。 {gmaps.length ? gmaps.map((marker: Gmap ) => ( <Marker key={marker.id} position={{ lat: marker.latitude, lng: marker.longitude, }} onMouseOver={() => { setSelected(marker); }} /> )) : null} アプリの詳しい説明は省きますが三項演算子で空配列を評価したい場合はこんな感じです。 gmapsが空配列の場合は表示されるのはnullになってコンポーネントは評価されません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Native】AlertやらDialogやらmodalやら似たようなやつのそれぞれ特徴をまとめました。

似たような使い方をするAlertとDialogとModalの違い ログインに失敗した時のエラーメッセージやユーザーに確認をとる時に使われるUIの作り方は何種類かあります。 その中で今回はReactNativeの標準に入っているAlert、ModalとライブラリのDialogの三つのそれぞれの違いをまとめました。 基本的にこの三つはほとんど同じUIで挙動や実装方法も同じです。ただ、Dialogじゃないとできないことなどもありましたので資料にしました。 ライブラリをインストール dialog yarn add react-native-dialog Github:https://github.com/mmazzarolo/react-native-dialog 実装 プロジェクトを作成し、各ライブラリをインポートします。 import React from 'react'; import {Alert, Button, View,Modal} from 'react-native'; import Dialog from "react-native-dialog"; 1、Alert 初めにAlertを作っていきます import React from 'react'; import {Alert, Button, View,Modal} from 'react-native'; import Dialog from "react-native-dialog"; export const AlertDialog = () => { const onPressAlert = () => { Alert.alert( 'アラートを出しました', 'テキストテキストテキストテキスト', [ { text: 'Cancel', onPress: () => console.log('アラートのcancelをタップした時の挙動を書く'), style: 'cancel', }, {text: 'OK', onPress: () => console.log('アラートのOKをタップした時の挙動を書く')}, ]); }; return ( <View> <Button title="Alert" onPress={onPressAlert} /> </View> ); }; Buttonをタップした時にAlertを表示させます。 配列の一つ目にstyle:'cancel'と書かれていますが、これは表示する文字の色、太さを変更することができます。 cancelだと太文字に、destructiveだと赤文字になります。 こんな感じで表示されます。  次に配列内のボタンを一つ増やしてみます。 Alert.alert('アラートを出しました', 'テキストテキストテキストテキスト', [ { text: 'setting', onPress: () => console.log('アラートのsettingをタップした時の挙動を書く'), style: 'destructive', }, { text: 'Cancel', onPress: () => console.log('アラートのcancelをタップした時の挙動を書く'), style: 'cancel', }, { text: 'OK', onPress: () => console.log('アラートのOKをタップした時の挙動を書く'), }, ]); すると。。。。 配列内が三つ以上の時は縦並びになります。 ここで一つ気になる点があります。 配列は一番目に「setting」、二番目に「Cancel」、三番目に「OK」としているのに実際はCancelが一番下になってます。 これは「Cancel」内のstyleに’cancel’としていると思いますが、cancelというstyleを当てると、例え一番目に入れていても表示する時は自動的に一番下になります。 便利っちゃー便利かな。 ちなみに配列を一つだけにするとこんな感じです Alertの特徴 1、アラートの上に出すタイトル(テキスト)のスタイリングができない →もし色やフォントサイズを変更したくても変えれないです。 2、タイトルに画像やアイコンを入れられない →基本テキストした入れないと思いますが、画像を入れたくても入れれないです。 3、手軽に実装できるので特に変わった実装が必要じゃない時はこれで十分かな →できることは少ないですが、Alertの内容だけで十分なことがほとんどだと思うので、基本的にはこれを使えばいい 補足ですが、実装するときにアラートの第二引数(テキスト)を入れたくなくて消してしまうと Alert.alert('アラート',[ { text: 'setting', onPress: () => console.log('アラートのsettingをタップした時の挙動を書く'), style: 'destructive', }, { text: 'Cancel', onPress: () => console.log('アラートのcancelをタップした時の挙動を書く'), style: 'cancel', type:'login-password', }, { text: 'OK', onPress: () => console.log('アラートのOKをタップした時の挙動を書く'), }, ], ); こんな感じに思ったUIにならないので、二行目にテキストが必要ない場合でも Alert.alert('アラート','',[ { text: 'setting', onPress: () => console.log('アラートのsettingをタップした時の挙動を書く'), style: 'destructive', }, { text: 'Cancel', onPress: () => console.log('アラートのcancelをタップした時の挙動を書く'), style: 'cancel', type:'login-password', }, { text: 'OK', onPress: () => console.log('アラートのOKをタップした時の挙動を書く'), }, ], ); こんな感じで空にしといてください! Dialog 実装していきます import React,{useState} from 'react'; import {Alert, Button, View,Modal} from 'react-native'; import Dialog from 'react-native-dialog'; export const AlertDialog = () => { const [visible,setVisible] = useState(false) return ( <View> <Button title="Alert" onPress={()=>setVisible(true)} /> <View> <Dialog.Container visible={visible}> <Dialog.Title>ダイアログを出しました</Dialog.Title> <Dialog.Description> あああああああああああああああああああああああああああああああああああ </Dialog.Description> <Dialog.Button label="Cancel" color='red' onPress={()=>setVisible(false)} /> <Dialog.Button label="OK" onPress={()=>setVisible(false)} /> </Dialog.Container> </View> </View> ); }; Alertではタップした時にAlertを出すようにしましたが、Dialogではvisibleがtrueの時に表示、falseで非表示にします。 ですのでuseStateを使ってtrueかfalseかを制御します。 Buttonをタップした時にtrueにし、ダイアログ内のボタンをタップすることでfalseとなり、非表示になります こんな感じで表示されます。Alertとほぼ同じですね。 では色々Dialogの特徴を紹介していこうと思います。 <View> <Dialog.Container visible={visible}> <Dialog.Title style={{fontSize:30,marginBottom:40}}>ダイアログを出しました</Dialog.Title> <Dialog.Description> あああああああああああああああああああああああああああああああああああ </Dialog.Description> <Dialog.Button label="Cancel" color='red' onPress={()=>setVisible(false)} /> <Dialog.Button label="OK" onPress={()=>setVisible(false)} /> <Dialog.Button label="Setting" bold={true} onPress={()=>setVisible(false)} /> </Dialog.Container> </View> まずスタイリングですがtitleにもstyleを当てることができます。またボタンの方はカラーや太さを調整することができます。 こんな感じに表示されます。 Alertの時は三つボタンがあると縦並びになりましたが、Dialogだと横並びに続いていきます。 さらにこんな事もできます <View> <Dialog.Container visible={visible}> <Dialog.Title style={{fontSize:30,marginBottom:40}}> <Icon name="user-o" size={70} /> </Dialog.Title> <Dialog.Input placeholder='名前' /> <Dialog.Button label="Cancel" color='red' onPress={()=>setVisible(false)} /> <Dialog.Button label="OK" onPress={()=>setVisible(false)} /> <Dialog.Button label="Setting" bold={true} onPress={()=>setVisible(false)} /> </Dialog.Container> </View> Dialog.title内をテキストではなく、アイコンを表示させています。 Alertではできませんでしたが、画像やアイコンも入れることができるんです。 そのほかにもDialog.inputでテキスト入力欄をダイアログにつけることができます! Dialogの特徴 1、Alertよりできることが多い 2、スタイルを当てることができる 3、画像やアイコンを入れることができる Modal 最後のModalです、実装していきます import React, {useState} from 'react'; import {Alert, Button, View, Modal, StyleSheet, Text} from 'react-native'; import Dialog from 'react-native-dialog'; import Icon from 'react-native-vector-icons/FontAwesome'; export const AlertDialog = () => { const [modalVisible, setModalVisible] = useState(false); return ( <View> <Button title="タップ" onPress={() => setModalVisible(true)} /> <View> <Modal animationType="slide" transparent={true} visible={modalVisible} onRequestClose={() => { Alert.alert('Modal has been closed.'); setModalVisible(!modalVisible); }}> <View style={styles.centeredView}> <View style={styles.modalView}> <Text style={styles.modalText}>Hello World!</Text> <Button title="閉じる" onPress={() => setModalVisible(false)} /> </View> </View> </Modal> </View> </View> ); }; const styles = StyleSheet.create({ centeredView: { flex: 1, justifyContent: 'center', alignItems: 'center', marginTop: 22, }, modalView: { margin: 20, backgroundColor: 'white', borderRadius: 20, width:300, padding: 35, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 5, }, button: { borderRadius: 20, padding: 10, elevation: 2, }, buttonOpen: { backgroundColor: '#F194FF', }, buttonClose: { backgroundColor: '#2196F3', }, textStyle: { color: 'white', fontWeight: 'bold', textAlign: 'center', }, modalText: { marginBottom: 15, textAlign: 'center', }, }); 特徴はAlertやDialogと違い、ほぼほぼスタイリングで自分で実装しているところです。 よくも悪くも自分の理想のものを作ることができます。 UIはこんな感じになります。 表示方法はDialogと同様にtrueの時は表示、falseで非表示としています。 ボタンをタップした時、モーダルは下から出てきます。 これはModalタグ内のanimatedTypeで設定しています。 現在はslideにしていますが、noneにすることでAlertやDialogと同じような感じで表示されます。 二つ目のtransparentはtrueかfalseで設定します。 trueの場合:モーダルを出した時に背景をもともと設定している色のままにする falseの場合:モーダルを出すときは背景色を決めていても画面全体を真っ白にする modal内はViewタグを使って一からスタイリングしているため、自分の思うようなものを作ることができますが、AlertやDialogに比べてめんどくさいです。 Modalの特徴 1、UIは自分で一から構築 2、自由度は高いがAlertやDialogに比べて実装がめんどくさい。 結論 以上三つの似たUIを作ってみましたが、僕的にはDialogが一番使いやすいと思いました。 ただ、OKとキャンセルを出すだけだったらAlertで十分です。 Modalは使うことはないかなと思いました。Dialogを使えばやりたいことはできると思いますので。 ぜひ参考にしてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

window.confirmのように手軽に使用できるReactの確認ダイアログコンポーネントを作成してみました

背景 ちょっと確認ダイアログを出したい際にwindow.confirmのように手軽に使用できるReactコンポーネントがあると便利だと思い、作ってnpmレジストリで公開してみました。 作ったもの GitHub: kamiya-kei/simple-confirm DEMO: https://kamiya-kei.github.io/simple-confirm/ npm: @kamiya-kei/simple-confirm オプションでダイアログタイトル・メッセージ・OK/キャンセルのテキスト・カラーを設定できるようにしてます。 インストール・使用方法 以下GitHubのREADMEにインストール・使用方法を記載していますので、よかったら使ってみてください。 kamiya-kei/simple-confirm/README.md 制作メモ window.confirmは以下の様に使用すると「OK」を選択するとtrue、キャンセルを選択するとfalseが返ってきます。 const is_ok = window.confirm('本当によろしいですか?'); 同じような感じで以下のように使用できるようにしました。 const is_ok = await simpleConfirmRef.current.confirm(); 子コンポーネント側の関数を呼び出せるようにする為にReact.useImperativeHandleを使用し、結果を同期的に受け取れるようにする為にPromise・Async/Awaitを使用しました。 上記コードを実行すると、下記コードのconfirm関数が呼び出されるようになっています。 useImperativeHandle(ref, () => ({ confirm: () => new Promise((resolve, reject) => { setHandleAgree(() => () => { resetHandle(); resolve(true); }); setHandleDisagree(() => () => { resetHandle(); resolve(false); }); setHandleClose(() => () => { resetHandle(); resolve(false); }); setOpen(true); }), })); コンポーネント全体のソースコードはこちらです。 https://github.com/kamiya-kei/simple-confirm/blob/main/src/index.js 最後に そもそもuseImperativeHandleは手続き的であり、あまり積極的に使用するべきではないといったことも書かれていたのですが、確認ダイアログを表示してその選択結果によって処理するといったことをする場合に宣言的かつシンプルで分かりやすいコードの書き方が思いつかず、手続き的な書き方をしました。 Reactは最近使い始めたばかりでまだ分かっていない部分も多いので、もっと勉強してより良い書き方ができるようにしていきたいと思います。 参考記事 React 16.8: 正式版となったReact Hooksを今さら総ざらいする Reactコンポーネントをnpmで公開する(GitHub Pages付き、Babel7、webpack4) async/awaitを使用して、"regeneratorRuntime is not defined "エラーが出た時の対処 npmとyarnのコマンド早見表
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React RouterのuseParamsがcssの設定を初期化しやがって10時間苦しんだ話

ReactRouterのドキュメントにあるuseParams1をそのまま試しても自分のプロジェクトではうまく動作しませんでした。 試したコードは以下になります。 index.tsx import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter as Router, Switch, Route, useParams } from "react-router-dom"; function BlogPost() { let { slug } = useParams(); return <div>Now showing post {slug}</div>; } function HomePage() { return <div>HomePage</div>; } ReactDOM.render( <Router> <Switch> <Route exact path="/"> <HomePage /> </Route> <Route path="/blog/:slug"> <BlogPost /> </Route> </Switch> </Router>, document.getElementById("root") ); HomePage(/)だとreset.cssが効いて、自分で定義していた全体に反映されるindex.cssも効いていました。 ところが、BlogPost(/blog/hello-world)に遷移した時にどちらのcssも効かなくなってしまっていました。 自分で定義したcss(reset.cssとindex.css)はpublicフォルダ内に保存されていて、index.html内で適用されていました。 暫定的な解決方法 index.cssとreset.cssをindex.tsx内で呼び出すようにしてうまくcssが適用されるようになりました。 index.tsx import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter as Router, Switch, Route, useParams } from "react-router-dom"; import "./index.css"; import "./reset.css"; function BlogPost() { let { slug } = useParams(); return <div>Now showing post {slug}</div>; } function HomePage() { return <div>HomePage</div>; } ReactDOM.render( <Router> <Switch> <Route exact path="/"> <HomePage /> </Route> <Route path="/blog/:slug"> <BlogPost /> </Route> </Switch> </Router>, document.getElementById("root") ); https://reactrouter.com/web/api/Hooks/useparams ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】関数コンポーネントで親のstateを子にpropsとして渡す方法

はじめに Reactで関数コンポーネントを使い、親のstateを子にpropsとして渡す方法(stateのリフトアップ)についてまとめました。 この記事では、1回クリックするとテキストがbananaからappleに変更されるボタンを作成します。 説明用に極力シンプルなコードにしています。 ご自身のコードに置き換えて頂いて、ご参考になればと思います。 また、この記事の内容に不備が御座いましたらご指摘頂けますと幸いです。 何の為に親のstateを子にpropsとして渡すのか 複数の子コンポーネントからデータを集めたり、コンポーネント同士を互いにやりとりさせる為には、 親コンポーネント内で共有のstateを宣言する必要があります。 環境 React:17.0.2 node.jsとnpmがインストールされていることが前提です。 準備 npx create-react-app my-app cd my-app npm start ターミナルの新しいウィンドウを開き、以下のコマンドを実行して必要なディレクトリとファイルを作成します。 mkdir src/components touch src/components/{Fruits.jsx,FruitsButton.jsx} 実装 まずは、stateのリフトアップをしないで書いてみます。 {/* Fruits.jsx */} import React, { useState } from "react"; const Fruits = () => { const [fruit, setFruit] = useState("banana"); return <button onClick={() => setFruit("apple")}>{fruit}</button>; }; export default Fruits; {/* App.js */} import "./App.css"; import Fruits from "./components/Fruits"; {/* 追加 */} function App() { return ( <div className="App"> <header className="App-header"> <Fruits /> {/* 追加 */} </header> </div> ); } export default App; stateのリフトアップをするとこうなります。 {/* Fruits.jsx(親コンポーネント) */} import React, { useState } from "react"; import FruitsButton from "./FruitsButton"; const Fruits = () => { const [fruit, setFruit] = useState("banana"); {/* 状態を"apple"に変える関数を定義*/} const ReplaceFruit = () => { setFruit("apple"); }; {/* propsとして子に渡すのは{fruit} は(現在の状態"banana")とReplaceFruit関数 */} return <FruitsButton fruit={fruit} onClick={ReplaceFruit} />; }; export default Fruits; {/* FruitsButton.jsx(子コンポーネント) */} const FruitsButton = (props) => { return <button onClick={() => props.onClick()}>{props.fruit}</button>; }; export default FruitsButton; 既に"apple"になっている方は一度ブラウザをリロードして下さい。 参考にしたサイト ステートフックの利用法 – React https://ja.reactjs.org/docs/hooks-state.html 06 新・日本一わかりやすいReact入門【基礎編】コンポーネントの状態管理 https://youtu.be/dTghhYtvPek
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React]Module not found: Can't resolve '../images/**' 画像が読み込めないエラー対処

問題 Reactで画像を表示させるために、localに画像ファイルを保存し、importすると以下のエラーがでた。 Module not found: Can't resolve '../images/**' in 'C:\Users....' 正しいパスでインポートしているはずなのになぜ、、、 解決策 下記に書き直すだけでなおった。 qiita.rb import Image from '../images/**.png' //pngをつける export const Result = () => { return ( //.... <img src={Image} /> ); }; 予測ででたパスを指定したので、絶対間違ってすこし詰まった? ちなみに Reactで画像を表示するには 1.上記のようにimportする 2.srcにパス名を指定する の2つがあるようだがどちらが良いのか。。。 1は、importしているため画像に問題がった際にエラーになりすぐに気づける。特に問題がなければ1の方がよさそう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactによるコンポーネントパターンまとめ

Reactによるコンポーネントパターンまとめ コンポーネントのパターンとして、Render-props, HOCs, Compound components, Container componentsの4種を紹介します。 またそれぞれのパターンに対してサンプルコードとCodeSandboxへのリンクを貼っています。 Render props 主にReactNodeをプロパティとして受け取り、それをrenderするパターンです。 サンプル renderプロパティで受け取ったものを表示するサンプルです。 function RenderPropsSample(props: {render: ReactNode}) { return <>{props.render}</> } 実用的な例 複数のrenderプロパティを持たせて、Union型を表示するようなコンポーネントを作ります。 例えば、下のようなPromiseのラッパーを用意します。 export type PromiseResult<T> = | { type: "rejected"; value: Error } | { type: "pending" } | { type: "fulfilled"; value: T }; これを表示するコンポーネントを作ろうと思ったら、promiseResultとその状態ごとのハンドラを受け取るコンポーネントを用意します。 こうすることで、promiseResultのパターンマッチのみを一つのコンポーネントに切り出すことができます。 interface PromiseRendererProps<T> { promiseResult: PromiseResult<T>; onPending?: ReactNode; onRejected?: (error: Error) => ReactNode; onFulfilled?: (value: T) => ReactNode; } export function PromiseResultRenderer<T>(props: PromiseRendererProps<T>): ReactElement { const { promiseResult, onPending, onRejected, onFulfilled } = props; switch (promiseResult.type) { case 'pending': return <>{onPending}</> case 'rejected': return <>{onRejected ? onRejected(promiseResult.value) : undefined}</> case 'fulfilled': return <>{onFulfilled ? onFulfilled(promiseResult.value) : undefined}</> } } 下のサンプルでは、10秒のロード時間がかかるasync関数を実行し、Pending中は「Loading...」と表示してFulfilledとなったら結果を表示しています。 尤も、このようなPromiseを表示するテクニックは今後Suspenseに置き換えられていきそうです。 Higher-order components コンポーネントを作る関数です。 propsを加工して渡したり、デフォルト値を埋めて渡す場合にはHOCsが便利です。 サンプル 引数で受け取ったコンポーネントをそのまま返す高階関数です。 function higherOrderComponent<P extends {}>(WrappedComponent: ComponentType<P>) { return (props: P) => <WrappedComponent {...props}/> } 実用的な例 disabledプロパティのデフォルト値をfalseではなくtrueにする高階コンポーネントを定義しています。 withDisabledByDefaultに渡すコンポーネントは disabled プロパティを持っていなければなりません。 interface RequiredProps { disabled?: boolean; } function withDisabledByDefault<P extends RequiredProps>( WrappedComponent: React.ComponentType<P> ) { return (props: P) => { return <WrappedComponent disabled={true} {...props} />; }; } Compound components 暗黙のうちに状態を共有する、親子のコンポーネントです。例えばulタグとliタグのようなイメージです。 状態は親が持ち、contextを使って共有します。 サンプル olとli要素をラップし、最後にクリックされた子コンポーネントのvalueを親コンポーネントが保持するサンプルです。 stateをまるごとcontextに渡して子コンポーネントに共有しています。 このcontextは親子の間でのみ共有し、外部には公開しないようにしましょう。 interface SelectedContextValue { selected: number | null; setSelected: (selected: number | null) => void; } const defaultSelectedContext: SelectedContextValue = { selected: null, setSelected: () => {} }; const SelectedContext = createContext<SelectedContextValue>( defaultSelectedContext ); export function SelectableList(props: PropsWithChildren<{}>) { const [selected, setSelected] = useState<number | null>(null); return ( <SelectedContext.Provider value={{ selected, setSelected }}> <h2>You are selecting {selected ?? "nothing"} now.</h2> <ol>{props.children}</ol> </SelectedContext.Provider> ); } function useSelectedContext() { const context = useContext(SelectedContext); return context ? context : defaultSelectedContext; } export function SelectableListItem(props: PropsWithChildren<{ value: number }>) { const { selected, setSelected } = useSelectedContext(); const handleClick = useCallback(() => { setSelected(props.value); }, [props.value, setSelected]); const additionalMessage = props.value === selected ? "I'm selected! " : ""; return ( <li onClick={handleClick}> {additionalMessage} {props.children} </li> ); } Container components 見た目には一切影響を与えない、副作用のみを扱うコンポーネントです。 スタイルの設定やPureなロジックを持つPresentationalコンポーネントに対して、例えばfetchやuseSelectorで得た値を渡したり、イベントハンドラにfetchやuseDispatchなどを使った関数を渡したりします。 サンプル ある項目のリストを表示するPresentationalコンポーネントと、サーバーから値を取ってきてそれに渡すContainerコンポーネントのサンプルです。本質的ではないのでサーバーとの通信は行っていませんが、この状態でもモックとしての価値があります。 function Presentational(props: { rows: string[] }) { return ( <ul> {props.rows.map((row) => { return <li>{row}</li>; })} </ul> ); } function ContainerMock() { const sampleRows = ["Hello", "Bonjour", "Guten tag"]; return <Presentational rows={sampleRows} />; } おわり Reactでコンポーネントを作成する時に鉄板となる手法を実例と共に紹介しました。 これらはいずれも主に見た目とロジックを分離して、それぞれの再利用性と高めたり、テストをしやすくしたりするのに役立ちます。 ぜひうまく活用して、見通しのいいフロントエンド開発を楽しみましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React] [Amplify] Audioでmp3を読み込めず「DOMException: Failed to load because no supported source was found.」が発生する

はじめに Reactで開発しているWebページ上で音楽を再生しようとしたところ ローカル環境では再生が可能であるのに、Amplifyでホストしている開発環境でタイトルのエラーが発生して再生できませんでした。 原因を調査しましたので、備忘録として記載していきます。 事象 以下コードのように音声を再生しようとしたところ コンソール上にエラーが表示されてしまいました。 import AudioSource from './assets/AudioSource.mp3'; ~中略~ const audio = new Audio(AudioSource); audio.play(); 以下のようなエラーが発生。サポートしてないソースだからロードできないそうな。なぜ? DOMException: Failed to load because no supported source was found. 解決策 普通にエラーメッセージでググるといかにもそれっぽいaudioやvideoタグを使用した際の内容が引っかかりますが、私には当てはまりませんでした。 私の場合はAmplifyの設定に問題がありました。 と言ってもSPAの設定時にAWS公式の以下の指定に沿って、SPAを機能させるためにリダイレクトの設定をしていただけです。 https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/redirects.html#redirects-for-single-page-web-apps-spa ふと気になって正規表現を見てみると、「mp3」が存在しません。 構築時に何となく設定をしていただけでしたので、全く気づきませんでした。 </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json)$)([^.]+$)/> というわけで、mp3をリダイレクトの対象外に追加してあげて解決しました。 </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json|mp3)$)([^.]+$)/> 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js】 Vercelビルド時のエラー解決方法 'Page Without Valid React Component'

使用環境 ・MacOS BigSur(11.5.2) ・VScode(1.59.1) ・Node.js (16.1.1) ・React(17.0.2) ・Next.js(11.1.0) はじめに Vercelのビルド時に出たエラー’Page Without Valid React Component’の解決方法です。 04:50:30.440 > Build error occurred 04:50:30.442 Error: Build optimization failed: found pages without a React Component as default export in 04:50:30.442 pages/editPage 04:50:30.442 pages/profile 04:50:30.442 pages/search 04:50:30.442 pages/userAuth 04:50:30.443 See https://nextjs.org/docs/messages/page-without-valid-component for more info. 04:50:30.443 at /vercel/path0/node_modules/next/dist/build/index.js:597:19 04:50:30.444 at processTicksAndRejections (internal/process/task_queues.js:95:5) 04:50:30.444 at async Span.traceAsyncFn (/vercel/path0/node_modules/next/dist/telemetry/trace/trace.js:60:20) 04:50:30.473 error Command failed with exit code 1. 04:50:30.474 info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. 04:50:30.493 Error: Command "yarn run build" exited with 1 結論/解決策 Vercelビルド時のエラーログだけではよく分からなかったので、エラーログに表示されていたNext.jsの公式ドキュメントのURLに飛んだら方法が書いてありました。 pagesの配下のReactコンポーネント(ページとして表示させたいコンポーネント)はexportではなく、export default を使用しなければいけないそうです。 If the file is meant to be a page, double check you have an export default with the React Component instead of an export. If you're already using export default, make sure the returned value is a valid React Component. - 公式DOCより (後々、エラーログみたらちゃんと書いてありました Error: Build optimization failed: found pages without a React Component as default export in) 最後に default exportだとimportする際のクラス名や関数名が一致していなくても呼び出せることから、予期せぬエラーが生じるのを回避するためにexportを使った方がいいという情報をどこかでみたので慣習的にそうしていました。 しかし今回のようにNext.jsでpagesのコンポーネントとして読み込ませる時など、default exportでなければValid React Componentとしてみなされないケースもあるみたいです。 なぜそうなのか気になって公式ドキュメントのPagesの解説をみましたが、詳しくは書いてありませんでした。これ以上深堀まではしてませんが、そういうものだと思っておきます。 もし詳しい方がいたら情報いただけると幸いです ? 参考資料 Next.js 公式ドキュメントPage Without Valid React Component Hatena Blog 【React/ES6】named exportsとdefault exportsの使い分け
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptとReactHooks

はじめに 去年からフロントは React、バックエンドは Django(DRF)で何か作れないのかということをやっているのですが TypeScript から逃げれないという圧も感じつつあったのですが、割とこのあたりはできる人は当たり前にやっていくので情報も少なくどうしたものかと悩んでいたのですが、 [基礎編]React Hooks + Django REST Framework API でフルスタック Web 開発 こういう講座を見つけて、同じ講師の方の派生の講座もうまいこと React 周りのフロント技術の良い講座があったり、Django との連携・テストまで解説している講座があったのでこれはいい機会だと思って、今まで独学でやってきたことを整理・リファクタリングする意味も込めてちょっと体系的に勉強し始めたのでその覚書です。 実は開発を体系的に収めたいというのには、AWS SAA を取ったのでその知識を腐らせたくないということと派生の DVA や DevOps の資格を取るために、そこで出題されるサービスをハンズオンでやりたいというのもあるのでそのためには……という意図もあったりします。 以下、上記講座の内容を自分で TS 化していて詰まったところとか、適切とは言えない解決の仕方をしているところを少し取り上げていきます。 作業していたリポジトリは以下の通り。 react-django-beginners react-django-beginners-DRFAPI useState の場合 まず最初に詰まったのはこのパターン。 以下のパターンくらいまでは問題なかったのですが // いわゆるプリミティブ型と言われるタイプ。初期値を設定すれば型推論してくれる const [text, setText] = useState(""); const [number, setNumber] = useState(1); const [textlist, setTextList] = useState(["sample", "text"]); const [numberlist, setNumberList] = useState([1, 2, 3, 4]); const [user, setUser] = useState({ name: "user", password: "testuser", version: "1.5", }); // プリミティブでもnullを含む場合は定義をしておくほうがいい const [count, setCount] = useState<number | null>(null); // 初期値を空のリストにしたい場合は定義をしておく const [textlist, setTextList] = useState<string[]>([]); const [numberlist, setNumberList] = useState<number[]>([]); const [multilist, setMultiList] = useState<string | number[]>([]); 下記のようなパターンを理解するのにちょっと悩みました。 なお、以後の項目ではさらに型の指定に悩む部分が出てきますがそれはその時に // 例えばAPIから以下のようなデータを受け取り、それをuseStateで扱いたいとする。 interface Task { id: number; title: string; } // その場合まずAPIからの返り値はまず、一括取得等Objectのリストになる場合はGenericを使ってinterface[]で定義。ここまではいい。 const [tasks, setTasks] = useState<Task[]>([]); // 次に単一データの場合は初期値が空にしたいがこれだと当然NG(interface側でプロパティに?をつけていたら問題ない) const [selectedtask, setSelectedTask] = useState<Task>({}); // これならOKだが、無闇に型アサーションを使うのは良くない const [selectedtask, setSelectedTask] = useState<Task>({} as Task); // なのでanyを使うか、適切な初期値を設定するのがベターなのだろうか? // ただし,プロパティの初期値を設定する場合はid:""でもid:nullでもstring型として扱われてエラーが出るのでanyにするしかなさそう const [selectedtask, setSelectedTask] = useState<any>({}); // 以下の場合もanyを使ったが………本当にいいのだろうか? const [editedTask, setEditedTask] = useState<any>({ id: "", title: "", }); // inputタグからの値は文字列扱いになるのでこうするしかない。またはNumber型に変換してからsetStateするか。 const [id, setId] = useState<string | number>(1); useContext+useReducer の場合 ポイントだと思った点は ACTIONTYPE にも型を定義する Reducer の state の初期値はオブジェクトにする Context と Reducer 部分は 1 ファイルにまとめた方がいい createContext の型定義の扱い createContext の型定義の扱いは こういう知見 もあるので要研究な気がします。 App.tsx import React from "react"; import "./App.css"; import logo from "./logo.svg"; import AppContext from "./contexts/AppContext"; import { useReducer } from "react"; import CompB from "./components/CompB"; const initialState = { count: 0 }; // actionにあてるためにACTIONTYPEを型にしておく。payload(actionから任意の値などを引数としてもらってそれを使って返す場合のプロパティ)が必要な場合はそちらも。 type ACTIONTYPE = | { type: "add_1"; payload?: number } | { type: "multiple_3"; payload?: number } | { type: "reset"; payload?: typeof initialState }; const reducer = (currentState: typeof initialState, action: ACTIONTYPE) => { // typeプロパティを引数にする switch (action.type) { case "add_1": return { count: currentState.count + 1 }; case "multiple_3": return { count: currentState.count * 3 }; case "reset": return initialState; default: return currentState; } }; const App: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( // createContextを定義したファイルをインポートしてProviderとすることでvalueを子コンポーネント以下で受け取れるようにする // useReducerのアクションをグローバルに使いたい場合はまずuseReducerのstateとdispatchをvalueの引数にする <AppContext.Provider value={{ state, dispatch }}> <div className="App"> <header className="App-header"> <img src={logo} alt="logo" className="App-logo" /> Count {state.count} <CompB /> </header> </div> </AppContext.Provider> ); }; export default App; AppContext.tsx // useContextのためにまずはcreateContextを定義したファイルを作る import { createContext } from "react"; // Reducerをグローバルに使いたい場合の設定、同じことを書いているのでおそらくReducerのファイルと統合する方が吉 const initialState = { count: 0 }; type ACTIONTYPE = | { type: "add_1"; payload?: number } | { type: "multiple_3"; payload?: number } | { type: "reset"; payload?: typeof initialState }; // Context.Providerのvalueの引数は{}なので型アサーションする方が丸い。 // interfaceで定義してnullとのunionもありだが、初期値が必要なのと、nullオブジェクトは許容できませんというエラーも起きてしまうのでとりあえずアサーション。 const AppContext = createContext( {} as { //state:numberでもよし。ReducerのStateに当たる部分に応じて適切に state: typeof initialState; // dispatchの型はReact.Dispatchらしい。引数で<ACTIONTYPE>を指定する。 dispatch: React.Dispatch<ACTIONTYPE>; } ); export default AppContext; 上記の別パターン const initialState = { count: 0 }; type ACTIONTYPE = | { type: "add_1"; payload?: number } | { type: "multiple_3"; payload?: number } | { type: "reset"; payload?: typeof initialState }; interface AppContextInterface { state: typeof initialState; dispatch: React.Dispatch<ACTIONTYPE>; } const AppContext = createContext<AppContextInterface | null>(null); 実際にuseContextを用いてdispatch関数を行う子コンポーネント import React, { useContext } from "react"; import AppContext from "../contexts/AppContext"; const CompC = () => { // Context.Providerのvalueからパスしてもらう。 const { dispatch } = useContext(AppContext); return ( <div> {/* <p>test</p> */} {/* あとは普通にdispatchとして使う */} <button onClick={() => dispatch({ type: "add_1" })}>Add + 1</button> <button onClick={() => dispatch({ type: "multiple_3" })}> Multiple * 3 </button> <button onClick={() => dispatch({ type: "reset" })}>Reset</button>{" "} </div> ); }; export default CompC; import React from "react"; import CompC from "./CompC"; const CompB = () => { return ( <div> <CompC /> </div> ); }; export default CompB; Redux CombineReducer と useReducer の場合 このケースは一見先程のパターンと同じなのかと思ったらそれだと上手く行かずに詰まってしまったので、講師の方にお願いしてソースだけ頂いた。 結論から言うとReducer 部分で state に初期値のプロパティを引数として直接設定することで、型推論を促すということでした。 確かに各 Reducer の state 部分に入るプロパティの型自体は変わらないので typeof など回りくどいことをする必要はないなとは思いました。 この 1 件で TypeScript はなんとなく 決められる or 決めるべきところは型を定義して、推論に任せてもいいところはそれを促す という書き方をするものだなというのを感じました。 App.tsx import { useReducer, useCallback } from "react"; import "./App.css"; import rootReducer from "./reducers/index"; import { SELL_MEAT, SELL_VEGETABLE } from "./reducers/actionTypes"; function App() { const initialState = { reducerMeat: { numOfMeat: 30 }, reducerVegetable: { numOfVegetable: 25 }, }; const [state, dispatch] = useReducer(rootReducer, initialState); return ( <div className="App"> <header className="App-header"> <button onClick={useCallback(() => { dispatch({ type: SELL_MEAT }); }, [])} > Sell meat </button> Today's Meat: {state.reducerMeat.numOfMeat} <button onClick={useCallback(() => { dispatch({ type: SELL_VEGETABLE }); }, [])} > Sell vegetable </button> Today's Vegetable: {state.reducerVegetable.numOfVegetable} </header> </div> ); } export default App; 各Reducer import { ACTIONTYPE, SELL_MEAT } from "./actionTypes"; const reducerMeet = (state = { numOfMeat: 0 }, action: ACTIONTYPE) => { switch (action.type) { case SELL_MEAT: return { ...state, numOfMeat: state.numOfMeat - 1 }; default: return state; } }; export default reducerMeet; import { ACTIONTYPE, SELL_VEGETABLE } from "./actionTypes"; const initialState = { numOfVegetable: 0 }; const reducerVegetable = (state = initialState, action: ACTIONTYPE) => { switch (action.type) { case SELL_VEGETABLE: return { ...state, numOfVegetable: state.numOfVegetable - 1, }; default: return state; } }; export default reducerVegetable; import { combineReducers } from "redux"; import reducerMeat from "./reducerMeat"; import reducerVegetable from "./reducerVegetable"; const rootReducer = combineReducers({ reducerMeat, reducerVegetable, }); export default rootReducer; ACTIONTYPE export type ACTIONTYPE = { type: "SELL_MEAT" } | { type: "SELL_VEGETABLE" }; export const SELL_MEAT = "SELL_MEAT"; export const SELL_VEGETABLE = "SELL_VEGETABLE"; 外部 API やバックエンドとの連携(ex.DRF) ここでの悩みは主にイベントでやりとりする引数の型指定。 実際に自分なりに考えて書いたのが以下の通り。 要点は 外部 API 等からデータを持ってくるときは必ずそのデータ型を interface 等で用意する 上記は例えば元 Json データ等があるならそれを別ファイルに保存してインポートし、typeof などをするのもアリ 各イベントの型は VScode 上で hover して確認して適切に定義する。 どうしても困ったら any。ただし、ベターではあるかもしれないけどベストではない。 input タグ絡みの値は string 扱いになるので適宜対応が必要 onClick 等、各イベント属性でアロー関数定義の関数を呼び出す際は TypeScript だと Void 扱いになるので呼び出す際にもアロー関数で呼び出す といった感じ。 DrfApiFetch.tsx import React, { useState, useEffect } from "react"; import axios from "axios"; const DrfApiFetch = () => { import React, { useState, useEffect } from "react"; import axios from "axios"; const DrfApiFetch = () => { interface Task { // any型を嫌うならこっちにすべき? // id: number | string; id: number; title: string; } // APIからの返り値はまず、一括取得等のObjectのリストになる場合はinterfaceで定義してinterface[]で定義 const [tasks, setTasks] = useState<Task[]>([]); // 次に単一データの場合は初期値が空のObjectであった場合は当然型チェックでエラーが出るのでanyにしておく、理由は下記。 const [selectedtask, setSelectedTask] = useState<any>({}); // React.ChangeEvent<HTMLInputElement>型の要素が入るが、初期値を定義したいのでanyにしておく // これはinputタグから受け取る値がstring型になり、かつidプロパティの初期値は空にしたいのだが、そうするとその初期値自体もstring型になってしまうため const [editedTask, setEditedTask] = useState<any>({ id: "", title: "", }); // inputタグをEventHandlerしたいならこれで定義(inputタグからの値はstring扱いされる) const [id, setId] = useState<string | number>(1); const getTaskList = async () => { try { const res = await axios.get("http://127.0.0.1:8000/api/tasks/", { headers: { Authorization: "Token ~", }, }); setTasks(res.data); } catch (error) { console.log(error); } }; const ChangeIdHandler = (e: React.ChangeEvent<HTMLInputElement>) => { setId(e.target.value); }; const ChangeTitleHandler = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; const name = e.target.name; setEditedTask({ ...editedTask, [name]: value }); }; const getTask = async () => { try { const res = await axios.get(`http://127.0.0.1:8000/api/tasks/${id}`, { headers: { Authorization: "Token ~", }, }); setSelectedTask(res.data); } catch (error) { console.log(error); } }; const newTask = async (task: Task) => { const data = { title: task.title, }; try { const res = await axios.post(`http://127.0.0.1:8000/api/tasks/`, data, { headers: { "Content-Type": "application/json", Authorization: "Token ~", }, }); setTasks([...tasks, res.data]); setEditedTask({ id: "", title: "", }); } catch (error) { console.log(error); } }; const editTask = async (task: Task) => { try { const res = await axios.put( `http://127.0.0.1:8000/api/tasks/${task.id}/`, task, { headers: { "Content-Type": "application/json", Authorization: "Token ~", }, } ); setTasks( tasks.map((task) => (task.id === editedTask.id ? res.data : task)) ); setEditedTask({ id: "", title: "", }); } catch (error: any) { const errorMessage = error.message; console.log(errorMessage); } }; const deleteTask = async (id: number) => { try { const res = await axios.delete(`http://127.0.0.1:8000/api/tasks/${id}`, { headers: { Authorization: "Token ~", }, }); setTasks(tasks.filter((task) => task.id !== id)); setSelectedTask({}); if (editedTask.id !== id) { setEditedTask({ id: "", title: "" }); } } catch (error) { console.log(error); } }; useEffect(() => { getTaskList(); }, []); return ( <div> <ul> {tasks.map((task) => ( <li key={task.id}> {task.title} {task.id} {/* 定義した関数の返り値の型を指定していない場合(=void)、onClick属性でそのまま指定すると関数扱いにならないのでアローで呼び出し直す */} <button onClick={() => deleteTask(task.id)}> <i className="fas fa-trash-alt"></i> </button> <button onClick={() => setEditedTask(task)}> <i className="fas fa-pen"></i> </button> </li> ))} </ul> <p>Set Id</p> <input type="text" value={id} onChange={ChangeIdHandler} /> <br /> <button type="button" onClick={() => getTask()}> Get Task </button> <br /> <br /> <input type="text" name="title" value={editedTask.title} onChange={ChangeTitleHandler} placeholder="New Title" /> {editedTask.id ? ( <button type="button" onClick={() => editTask(editedTask)}> Edit Task </button> ) : ( <button type="button" onClick={() => newTask(editedTask)}> Create Task </button> )} <br /> <h3> {selectedtask.title} {selectedtask.id} </h3> </div> ); }; export default DrfApiFetch; 最後に TypeScript に本格的に触れてからまだ 1 週間も経ってないですが、慣れるまでにはなかなか戸惑うことが多いなと感じました。 ともあれ、一部助けをお借りしたものの自分で JS を TS 化していく中で色々と感じも掴めたのでそれを持って [JIRA 編]React Hooks/TypeScript + Django REST API で作るオリジナル JIRA こちらのより実践的な内容に行きたいと思いました。 参考 any 型で諦めない React.EventCallback Type Assertion(型アサーション) typescript-cheatsheets React の標準機能(useContext/useReducer)でステート管理[TypeScript 版] Interpreting Errors React に TypeScript を導入【Tips 集】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む