- 投稿日:2020-01-13T22:34:17+09:00
Material UIのヘッダー・フッターナビゲーションの実装【初学者のReact×Railsアプリ開発 第9回】
やったこと
- Material UIを用いて、ヘッダー・フッターナビゲーションを実装した
成果物
画面の横幅が大きいとき
画面の横幅が小さいとき
参考にさせていただいたサイト
- こちらのコードを参考にさせていただきました。 Reactで誰もがやりたかった10の機能。アプリ構想はあるけど作れない人の壁をぶっ壊す。
実装手順
- 上の記事のコードを参考に改造しただけなので、今回はそんなに工夫点はないです。
- 上の参考記事をご覧ください。
App.js
App.jsimport ResponsiveDrawer from './containers/ResponsiveDrawer'; import RouteRelatedBottomNavigation from './containers/RouteRelatedBottomNavigation'; class App extends Component { render() { return ( <div className="App"> <ResponsiveDrawer className="ResponsiveDrawer"> <Switch> <Route path="/login" component={Login} /> <Route path="/info" component={Info} /> <Route path="/term" component={Term} /> <Auth> <Switch> <Route exact path="/" component={Home} /> <Route path='/create' component={Create} /> </Switch> </Auth> </Switch> </ResponsiveDrawer> <RouteRelatedBottomNavigation /> </div > ); } }containers/RouteRelatedBottomNavigation.js
RouteRelatedBottomNavigation.jsimport React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import BottomNavigation from '@material-ui/core/BottomNavigation'; import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; import HomeIcon from '@material-ui/icons/Home'; import InfoIcon from '@material-ui/icons/Info'; import { Link, withRouter } from 'react-router-dom'; const styles = theme => ({ wrapper: { display: 'block', width: '100%', position: 'fixed', left: 0, bottom: 0, zIndex: 1000, textAlign: 'center', }, root: { [theme.breakpoints.up('md')]: { display: 'none', }, }, button: { maxWidth: '100%', }, }); class RouteRelatedBottomNavigation extends React.Component { buttons_info = [ { label: 'トップページ', icon: <HomeIcon />, link_to: '/' }, { label: 'テーマ一覧', icon: <InfoIcon />, link_to: '/postslist' }, { label: '投稿', icon: <InfoIcon />, link_to: '/create' }, ]; buttons = this.buttons_info.map((button_info, index) => { return ( <BottomNavigationAction value={button_info.link_to} label={button_info.label} className={this.props.classes.button} icon={button_info.icon} component={Link} to={button_info.link_to} /> ); }) render() { const { classes } = this.props; return ( <div className={classes.wrapper}> <BottomNavigation value={this.props.location.pathname} showLabels className={classes.root} children={this.buttons} /> </div> ); } } RouteRelatedBottomNavigation.propTypes = { classes: PropTypes.object.isRequired, }; export default withRouter( withStyles(styles, { withTheme: true })(RouteRelatedBottomNavigation) );containers/ResponsiveDrawer.js
ResponsiveDrawer.jsimport React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import Drawer from '@material-ui/core/Drawer'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import List from '@material-ui/core/List'; import IconButton from '@material-ui/core/IconButton'; import Hidden from '@material-ui/core/Hidden'; import Divider from '@material-ui/core/Divider'; import MenuIcon from '@material-ui/icons/Menu'; import Icon from '@material-ui/core/Icon'; import SettingsIcon from '@material-ui/icons/Settings'; import InfoIcon from '@material-ui/icons/Info'; import HomeIcon from '@material-ui/icons/Home'; // import titlepng from '../images/sukiraism_title.png' import { Link } from 'react-router-dom'; import ResponsiveDrawerListItem from '../components/ResponsiveDrawerListItem'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as actions from '../actions'; const drawerWidth = 240; const headerNavigationHeight = 56; const bottomNavigationHeight = 56; const styles = theme => ({ root: { flexGrow: 1, height: '100vh', zIndex: 1, overflow: 'hidden', position: 'relative', display: 'flex', width: '100%', }, appBar: { position: 'absolute', marginLeft: drawerWidth, [theme.breakpoints.up('md')]: { width: `calc(100% - ${drawerWidth}px)`, }, }, toolBar: { justifyContent: 'space-between', minHeight: bottomNavigationHeight, }, navIconHide: { [theme.breakpoints.up('md')]: { display: 'none', }, }, toolbar: theme.mixins.toolbar, drawerPaper: { width: drawerWidth, height: '100vh', [theme.breakpoints.up('md')]: { position: 'relative', }, }, content: { flexGrow: 1, backgroundColor: theme.palette.background.default, padding: theme.spacing.unit * 3, paddingTop: `calc(10px + ${headerNavigationHeight}px)`, paddingBottom: `calc(10px + ${bottomNavigationHeight}px)`, paddingLeft: 0, paddingRight: 0, [theme.breakpoints.up('md')]: { paddingBottom: 10, }, }, headerLogo: { display: 'flex', height: 40, width: 180, }, iconLogo: { display: 'flex', height: 40, width: 40, borderRadius: '50%', }, searchbar: { // flexGrow: 1, }, flex: { flexGrow: 1, }, }); class ResponsiveDrawer extends React.Component { constructor(props) { super(props); this.state = { mobileOpen: false, }; } closeDrawerNav = () => { this.setState({ mobileOpen: false }); } openDrawerNav = () => { this.setState({ mobileOpen: true }); } renderUserlink() { const { CurrentUserReducer } = this.props; const { classes } = this.props; const image = CurrentUserReducer.items.image; const isLoggedin = CurrentUserReducer.isLoggedin; const nicknameLink = "/users/" + CurrentUserReducer.items.nickname; if (isLoggedin) { return ( <Link to={nicknameLink}> <img src={image} alt="nickname_link" className={classes.iconLogo} /> </Link> ) } else { return ( <a href={process.env.REACT_APP_API127_URL + "/api/v1/auth/twitter?auth_origin_url=" + process.env.REACT_APP_BASE_URL} > <Icon>add_circle</Icon> </a > ) } } renderDeletewithCondition() { const { CurrentUserReducer } = this.props; const isLoggedin = CurrentUserReducer.isLoggedin; if (isLoggedin) { return ( <List> <ResponsiveDrawerListItem to="/deleteaccount" onClick={this.closeDrawerNav} icon={<SettingsIcon />} text="アカウントの削除" /> </List> ) } else { } } renderLogoutwithCondition() { const { CurrentUserReducer } = this.props; const isLoggedin = CurrentUserReducer.isLoggedin; if (isLoggedin) { return ( <List> <ResponsiveDrawerListItem to="/logout" onClick={this.closeDrawerNav} icon={<SettingsIcon />} text="ログアウト" /> </List> ) } else { } } render() { const { classes, theme, title, search } = this.props; const { CurrentUserReducer } = this.props; const isloggedin = CurrentUserReducer.isLoggedin; const drawer = ( <div> <List> <ResponsiveDrawerListItem to="" onClick={this.closeDrawerNav} icon={<InfoIcon />} text="ホーム" /> </List> <List> <ResponsiveDrawerListItem to="postslist" onClick={this.closeDrawerNav} icon={<InfoIcon />} text="テーマ一覧" /> </List> <List> <ResponsiveDrawerListItem to="create" onClick={this.closeDrawerNav} icon={<InfoIcon />} text="テーマを投稿する" /> </List> <List> <ResponsiveDrawerListItem to="search" onClick={this.closeDrawerNav} icon={<InfoIcon />} text="テーマを検索する" /> </List> <Divider /> <List> <ResponsiveDrawerListItem to="info" onClick={this.closeDrawerNav} icon={<InfoIcon />} text="XXXとは" /> </List> <List> <ResponsiveDrawerListItem to="/term" onClick={this.closeDrawerNav} icon={<HomeIcon />} text="利用規約" /> </List> <Divider /> {this.renderDeletewithCondition()} {this.renderLogoutwithCondition()} </div> ); return ( <div className={classes.root}> <AppBar className={classes.appBar} position="fixed"> <Toolbar className={classes.toolBar} variant="dense"> <IconButton color="inherit" aria-label="Open drawer" onClick={() => this.openDrawerNav()} className={classes.navIconHide} > <MenuIcon /> </IconButton> <Link to="/"> <img src="#" width="150px" alt="title_logo" className={classes.headerLogo} /> </Link> {this.renderUserlink()} </Toolbar> </AppBar> <Hidden mdUp> <Drawer variant="temporary" anchor={theme.direction === 'rtl' ? 'right' : 'left'} open={this.state.mobileOpen} onClose={this.closeDrawerNav} classes={{ paper: classes.drawerPaper, }} ModalProps={{ keepMounted: true, // Better open performance on mobile. }} > {drawer} </Drawer> </Hidden> <Hidden smDown implementation="css"> <Drawer variant="permanent" open classes={{ paper: classes.drawerPaper, }} > {drawer} </Drawer> </Hidden> <main className={classes.content}> {this.props.children} </main> </div> ); } } ResponsiveDrawer.propTypes = { classes: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; const mapState = (state, ownProps) => ({ CurrentUserReducer: state.CurrentUserReducer }); function mapDispatch(dispatch) { return { actions: bindActionCreators(actions, dispatch), }; } export default connect(mapState, mapDispatch)( withStyles(styles, { withTheme: true })(ResponsiveDrawer) );components/ResponsiveDrawerListItem.js
conponents/ResponsiveDrawerListItem.jsimport React from 'react'; import PropTypes from 'prop-types'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import { Link } from 'react-router-dom' const ResponsiveDrawerListItem = ({ to, onClick, icon, text }) => ( <ListItem button component={Link} to={to} onClick={onClick}> <ListItemIcon> {icon} </ListItemIcon> <ListItemText primary={text} /> </ListItem> ); ResponsiveDrawerListItem.propTypes = { to: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, icon: PropTypes.object.isRequired, text: PropTypes.string.isRequired, }; export default ResponsiveDrawerListItem;
- 投稿日:2020-01-13T22:31:57+09:00
typescript-fsa-redux-sagaことはじめ
typescript-fsaしゅごい
そんな便利な
typescript-fsa
には以下のCompanion Packagesが同じ作者より提供されています(リンク省略)
typescript-fsa-redux-saga
typescript-fsa-redux-observable
typescript-fsa-redux-thunk
typescript-fsa-reducers
この記事では
typescript-fsa-redux-saga
の使い方について簡単にまとめた記事になります(一部typescript-fsa
及びtypescript-fsa-reducers
が入ります)。本記事執筆時におけるバージョンは以下の通りです。
redux@4.0.5 redux-saga@1.1.3 typescript-fsa@3.0.0 typescript-fsa-reducers@1.2.1 typescript-fsa-redux-saga@2.0.0提供されているAPI
提供されているAPIはreadmeにも書かれていますがbindAsyncActionのみです。
bindAsyncAction
はAsyncActionCreator
を受け取ってHigherOrderSaga
を返す関数です。以下のような処理を行います。
- AACを受け取る
- 受け取った
SagaIterator
に引数paramsを渡して起動put(AAC.started({params}))
(省略可)- 受け取った
SagaIterator
が処理を終えreturn
/throw
すると
return
の場合、yield
したresultをput(AAC.done({params, result}))
throw
の場合、catch
しthrow
されたerrorを使ってput(AAC.failed({params, error}))
AsyncACtionCreator
AsyncActionCreator
(AAC)はtypescript-fsa
のactionCreator#async
で作られるオブジェクトです。
作り方等は私の記事で恐縮ですがこちらを参照ください。HigherOrderSaga
HigherOrderSaga
はSagaIterator
を受け取る関数です。
SagaIterator
はredux-saga
にて以下のように定義されています。export type SagaIterator<RT = any> = Iterator<StrictEffect, RT, any>
Iterator
の定義も見てみます。interface Iterator<T, TReturn = any, TNext = undefined> { // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. next(...args: [] | [TNext]): IteratorResult<T, TReturn>; return?(value?: TReturn): IteratorResult<T, TReturn>; throw?(e?: any): IteratorResult<T, TReturn>; }つまり
next()
した際にIteratorYieldResult<StrictEffect>
が返ってくるIterableオブジェクトが来れば良さそうです。次に
StrictEffect
について見てみます。export type StrictEffect<T = any, P = any> = SimpleEffect<T, P> | StrictCombinatorEffect<T, P>; export interface SimpleEffect<T, P = any> extends Effect<T, P> { combinator: false } export interface StrictCombinatorEffect<T, P> extends Effect< T, CombinatorEffectDescriptor<StrictEffect> > { combinator: true } export interface Effect<T = any, P = any> { '@@redux-saga/IO': true combinator: boolean type: T payload: P }ここから以下のことが分かりました
StrictEffect
はSimpleEffect
とStrictBombinatorEffect
の共用体型ということSimpleEffect
とStrictBombinatorEffect
はEffect
を継承しているということEffect
はFSAを拡張したものということ使ってみる
型パワーを実感するために使ってみます。
1秒かけて
string
の文字数を返すasync
関数をsagaで動かすことを考えてみます。また与えられた文字数が3文字の場合Error
をthrow
します。まずはAACとその
async
関数を定義します。// いつもの import actionCreatorFactory from 'typescript-fsa' const actionCreator = actionCreatorFacory() // AAC定義 const getLengthAction = actionCreator.async<string, number, Error>('getLengthAction') // async関数 const getLength = async (s: string): Promise<number> => { await new Promise(resolve => globalThis.setTimout(resolve, 1000)) if (s.length !== 3) { return s.length } else { throw new Error('error') } }次にsaga workerを定義します。
typescript-fsa-redux-saga
を使わない場合は似たような処理を以下のように書くと思います。function* getLengthWorker(s: string) { try { const result = yield call(getLength, s) yield put({type: 'SomeActionDone', payload: result}) } catch (e) { yield put({type: 'SomeActionFailed', payload: e, error: true}) } }
typescript-fsa-redux-saga
を使う場合は以下のように書きます。// saga worker function* getLengthWorker(s: string) { return yield call(getLength, s) } // saga workerをwrapしたworker const boundGetLengthWorker = bindAsyncAction(getLengthAction)(getLengthWorker)処理結果をreturnしていないことや例外をcatchしていないのでだいぶシンプルになっていると思います。
参考までに、
getLengthWorker
はSimpleEffect
を継承したCallEffect
を返すcall
をyield
し、その結果をreturn
しています。これによりIterator<SimpleEffect>
のシグネチャを満たしています。なので
function* getLengthWorker(s: string) { // return yield call(getLength, s) return yield s }のように
getLengthWorker
を書き換えるとgetLengthWorker
の型はIterator<string>
になりbindAsyncAction
のシグネチャを満たさずコンパイルエラーになります。次に
boundGetLengthWorker
の起動方法を見てみます。function* getLengthHandler(): SagaIterator { yield takeEvery(getLengthAction.started, function*({payload}) { yield call(boundGetLengthWorker, payload) }) } export function* rootSaga(): SagaIterator { yield fork(getLengthHandler) }
getLengthHandler
がgetLengthAction.started
を受け取るとboundGetLengthWorker
を起動します。
payload
はgetLengthAction.started
のpayloadの型なのでここでの型チェックを行うことができます。以上が大まかな流れです。
注意点
AAC.started
の罠さて、上記のサンプルには致命的なミスがあります。
getLengthHandler
はgetLengthAction.started
を受け取りboundGetLengthWorker
を起動していますが、そのboundGetLengthWorker
内部でもgetLengthAction.started
を発火させています。
そのためこのまま実行すると無限ループに陥ってしまいます。これを回避するためには以下の2つの手段が取り得ます。
- saga workerで
put(getLengthAction.started())
しないtakeEvery
で受け取るActionを変えるsaga workerで
put(getLengthAction.started())
しないこの方法は非常にスッキリ書けます。以下のように
bindAsyncAction
にoptionを渡すだけです。const boundGetLengthWorker = bindAsyncAction(getLengthAction, {skipStartedAction: true})(getLengthWorker)
takeEvery
で受け取るActionを変えるこの方法は書き方が冗長になりますがもう少し自由度をもたせることが可能です。
以下のようにsaga起動用のactionを別に定義します。// AAC定義 type getLengthProp = string const getLengthAction = actionCreator.async<getLengthProp, number, Error>('getLengthAction') // trigger用Action const getLengthOp = actionCreator<getLengthProp>('GetLengthOp') // ... function* getLengthHandler(): SagaIterator { yield takeEvery(hogeAsyncOp, function*({payload}) { yield call(boundGetLengthWorker, payload) }) }このようにすることで例えば
takeEvery
した後様々な前処理を行い、その後実行されるboundGetLengthWorker
が起動したタイミングを正確にput
したいというニーズを満たすことができます(すいません、このシチュエーションは相当なレベルのコーナーケースだと思います。他に良い例があれば教えてください・・・)。なおこの場合でも仮に
getLengthAction
のPropsの型とgetLengthOp
のPropsの型が異なった場合getLengthHandler
にて型エラーを検知できます。個人的には特段の事情が無い限りは
{skipStartedAction: true}
してAAC.started
をdispatch
するのが良いと思います。
AAC.failed
は型安全ではない実際に使用する際は
typescript-fsa-reducers
を合わせて以下のように使うと思います。import { reducerWithInitialState } from 'typescript-fsa-reducers' import { ErrorState } from '../containers/Error' const failedHandler = (_: unknown, { error }: { error: Error }): ErrorState => { return { error: { type: error.name, message: error.message }, } } export const errorReducer = reducerWithInitialState({ error: null, } as ErrorState) .case(getLengthAction.failed, failedHandler)reducer内では型安全が保証されています。
getLengthAction.failed
のpayloadは{ error: Error }
が含まれています。これは一見問題なさそうです。しかし例えば
getLengthAction
でbindされたsaga workerでcall
しているasync関数(getLength
)が以下のようになっている場合どうなるでしょうか。const getLength = async (s: string): Promise<number> => { throw 'a' }この場合は型エラーが出ません。javascriptでは全てのオブジェクトがthrowableなためです。
そのため丁寧にthrowしないと実行時エラーが起きてしまう危険があります。終わりに
以上を元に
typescript-fsa-redux-saga
について簡単にまとめてみます。
- メリット
try...catch...
のようなボイラープレートを削減できる- 型チェックできる部分が増える
- デメリット
- saga worker内で
throw
しないといけない個人的な所感ですが
throw
する事を許容する/しないという点が採用にあたってのポイントになると思います。
特にsaga内でcall
する非同期関数で謎のerrorをthrowされる危険を許容できないのであればtypescript-fsa-redux-saga
を採用せずtry...catch...
で手動でAACをput
するのも有りだと思います。また、現状
bindAsyncAction
はthrow
された時のみAAC.failed
をput
するという点も覚えておきたいところです。
- 投稿日:2020-01-13T21:38:50+09:00
API叩いてReactとサーバーサイドでやり取りしてみる
Laravel6 x React.js x Material-Ui で環境構築しモダン技術で開発
以前の記事で、構築した環境で、一覧表示機能を実装したので、記事を書いてみたいと思います。
Reactからaxiosでリクエスト投げて、LaraveでJSONを返して、そのデータを一覧表の中で表示してみたいと思います。React側で、まずデータを受け取るためのstateを定義する
表示したいコンポーネントの中にstateを定義します。
今回の場合は受け取ったデータがarrayのため、初期値も空配列で設定しています。
setPostsはpostsの値を変更する唯一のメソッドです。Home.jsfunction Home() { const [posts, setPosts] = useState([]); --------------------------------Reactからaxiosを使いリクエストを投げる
表示したいコンポーネントからaxiosを使用し、サーバーサイドにリクエストを飛ばします。
Home.jsaxios .get('/api/posts') //リクエストを飛ばすpath .then(response => { setPosts(response.data); }) //成功した場合、postsを更新する(then) .catch(() => { console.log('通信に失敗しました'); }); //失敗した場合(catch)Routeで呼び出すアクションを指定
Laravelのroutes/api.phpを開き、下記のコードを記載しcontrollerの、どのアクションを指定するか指定します。
api.phpRoute::group(['middleware' => 'api'], function(){ Route::get('posts', 'Api\PostController@index'); });呼び出したindex()でJSONを返す処理を記載
app/Http/Controllers/api/PostController.phpのindex()にJSONを返す記載
api.php<?php namespace App\Http\Controllers\api; use App\Http\Controllers\Controller; use App\Post; use Illuminate\Http\Request; class PostController extends Controller { public function index() { $posts = Post::all(); return response()->json($posts, 200); //型をJSONに指定します。 } }サーバーサイドから、データを確認してみる
DBのデータは下記の通り。
こちらのデータがJSON形式でサーバーサイドからresponse.dataとして返ってきて、reactのstateであるpostsを更新することになります。
確認してみます。Home.jsaxios .get('/api/posts') .then(response => { setPosts(response.data); console.log(response.data); })chromeで確認してみます。
ちゃんとデータとして返ってきている事がわかりますね。
このレスポンスデータの形でstateのpostsが更新されるため、例えばタスク1を表示する場合posts[0].name
で取得できるということです。表示するデータを含む配列を作ってJSXの中で使用し、表示
Home.jslet rows = []; posts.map((rowData) => rows.push({ user_name: rowData.name, post: rowData.content, btn: <Button color="secondary" variant="contained" key={rowData.id} href={`/post/edit/${rowData.id}`}>編集</Button>, deleteBtn: <Button color="primary" variant="contained" href="/" onClick={() => deletePost(rowData)}>完了</Button> }) ); return( <TableBody> {rows.map((row, index) => ( <TableRow key={index}> {Object.keys(row).map(function (key, index) { return ( <TableCell align="center" key={index}>{row[key]}</TableCell> ); })} </TableRow> <TableBody> );画面を確認しましょう。
きちんと表示されていることが確認できました。
リクエストを投げるタイミングを最初にページにアクセスしたときのみに変更する。
今の状態でも表示はできていますが、繰り返しaxiosのリクエストが走ってしまう設定担っています。
useeffectを使用することで、ページにアクセスしたときのみ、リクエストを送ることができます。axiosを投げる処理を関数化しuseEffect()の中で呼び出します。
第二引数を空配列に指定することで、空配列が渡ってきたときのみ実行することができます。Home.jsuseEffect(() => { getPostsData(); },[]) function getPostsData(){ axios .get('/api/posts') .then(response => { setPosts(response.data); }) .catch(() => { console.log('通信に失敗しました'); }); }今回の記事は以上です。
- 投稿日:2020-01-13T18:41:31+09:00
react にて、画像リンク切れの際、imgタグを非表示にする
通常のhtmlにおいては、onerrorハンドラにてリンク切れのimgタグを非表示にできます。
<img src="original.png" alt="title" onerror="this.style.display='none'"/>reactではこれができないため、以下のように
onError
ハンドラを作成します。<img src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} />サンプル
?以下のサンプルは、
create-react-app
直後のApp.js
を編集したものです。App.jsimport React from 'react'; function App() { return ( <div className="App"> <img border="1" src={`${process.env.PUBLIC_URL}/logo192.png`} /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onerror="this.style.display='none'" /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} /> </div> ); } export default App;参考
?Hide broken image link in Semantic UI React
?リンク切れの時に代替画像を表示したいときはこちら。
- 投稿日:2020-01-13T18:25:36+09:00
Laravel6 x React.js x Material-Ui で環境構築しモダン技術で開発
Laravelの環境でReact.jsを使ってみたいとの思いから、環境構築したのでその手順を残しておく。ついでにUIフレームワークも、Material-Uiを使用しましたので、その手順も残しておきます。Reactを使用することで、フロントエンドとバックエンドの責務を分離しAPIを用いて、やり取りします。今回はその環境構築部分です。
使用技術の簡単な紹介
- Laravel 近年人気が高まっているPHPフレームワーク。MVCで設計されており、筆者はRuby on Railsとも似ていると思います。
- React.js FACEBOOK発のJavaScriptライブラリ。Vue.jsと並び人気の高いライブラリである。
- Material-Ui 素早くシンプルで美しいデザインを構成できるgoogle発のUiフレームワーク。importすることで、簡単にボタンや表などの見た目を整える事ができます。Bootstrapのような見た目をReactで構成するようなイメージでいいと思います。(https://material-ui.com/ja/)
Laravel6の実行環境を構築する
初期画面が表示できれば、XAMPP等でも大丈夫なのですが、筆者はdockerを用いて開発しました。こちらの記事を参照することで、簡単に構築することができたので、紹介させていただきます。(https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4)
すでに、書籍等で何かしらのLaravel環境構築方法をご存知のかたはそちらで大丈夫ですが、Laravel6以降でreactの導入方法が異なっているため、Laravelのヴァージョン指定する場合は6.0以降にしてください。
下記画面が表示できたら、Reactの導入に進みましょう。React導入のため、node.jsの実行環境を用意する。
ターミナル.$ node -v v10.10.0 $ npm -v 6.4.1上記のコマンド実行し、バージョン確認できればOKです。
Node.jsの実行環境がない場合はこちら
https://qiita.com/PolarBear/items/62c0416492810b7ecf7cまた、yarnで管理している方は適宜、npmと読み替えながら進めてください。
React.jsの環境構築
Laravelの初期画面が表示できたら、プロジェクト直下で下記コマンドを実行します。
ターミナル.$ php artisan ui react //package.jsonにreact関連の記述がされます。 $ npm install //パッケージのインストールを行います。 $ npm run dev //ビルドできるか確認します。 DONE Compiled successfully //こちらが表示されたら成功です。次にjsファイルを読み込む設定をします。
welcome.blade.phpを開き、bodyタグの中の一番下、つまりbodyタグの閉じタグの直前に下記のコード2行(divとscript)を記載します。
welcome.blade.phpーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー抜粋 <div class="links"> <a href="https://laravel.com/docs">Docs</a> <a href="https://laracasts.com">Laracasts</a> <a href="https://laravel-news.com">News</a> <a href="https://blog.laravel.com">Blog</a> <a href="https://nova.laravel.com">Nova</a> <a href="https://forge.laravel.com">Forge</a> <a href="https://github.com/laravel/laravel">GitHub</a> </div> </div> </div> //下記の追記します。 <div id="root"></div> <script src="{{mix('js/app.js')}}"></script> </body>こちらのコードを追加することで、デフォルト設定のExample.jsのコンポーネントが読み込まれるため、画面左下に下記の記述が追記されます。
Example Component
I'm an example component!画像は以下
表示できていればLaravelの環境にreactを導入できています。
Material-Uiの導入
次にMaterial-Uiを導入します。
https://material-ui.com/ja/
公式に基づきnpmインストールします。
また使い方は公式に詳しく掲載されていますので、今回はButtonタグのみ用いて環境構築の確認を行います。ターミナル.$ $ npm install @material-ui/core //package.jsonにmaterial-Ui関連の記述がされます。 $ npm install //パッケージのインストールを行います。次にresources/js/components/Example.jsを開き、ファイル上部にMaterial-Uiのボタンタグをインポートし、先程Laravelの初期画面に表示された部分の下にimportした環境構築確認用ボタンを追記します。
Example.jsimport React from 'react'; import { Button } from '@material-ui/core'; //追記するコード function Example() { return ( <div className="container"> <div className="row justify-content-center"> <div className="col-md-8"> <div className="card"> <div className="card-header">Example Component</div> <div className="card-body">I'm an example component!</div> <Button color="primary" variant="contained">環境構築</Button> //追記するコード </div> </div> </div> </div> ); } export default Example;設置できたらファイルを保存しビルド
ターミナル.$ npm run build成功したら画面をリロード(キャッシュを消したほうが確実です。)し、初期画面を確認します。
先程のReactの環境構築確認用のコンポーネント部分に青色の環境構築用ボタンが確認できました。
ちなみにButtonタグの中のcolorをprimaryからsecondaryに変更することでボタンの色が赤色に変わります。Material-Uiの機能がきちんと動作していることが確認できましたね。。
環境構築は以上です。
- 投稿日:2020-01-13T14:14:31+09:00
Tinder風UIで「好き」「嫌い」を投票できる画面を実装する【React×Railsアプリ開発 第8回】
やったこと
- Tinder風UIで投稿に対して、「好き」か「嫌い」が投票できる画面を実装した。
- モジュールはreact-swipe-card-chsstmを使って、Tinder風UIを実装した。
今回の成果
できなかったこと
- react-swipe-card-chsstmのコードを修正して使いたかったが、どうしても修正後のモジュールが上手くインストールできなかった。
- Github上でForkしたあとにコード修正、再度インストールまでは良かったが、トランスパイル(コンパイル)が上手くいってないのかな〜...
実装手順
モジュールインストール
ここでインストール済み。npm install react-swipe-card-chsstmで行けるはず。
Rails APIの調整
- ログイン中ユーザーが「好き」か「嫌い」か投票していないポストをランダムで10個ずつ返すAPIを作る
- answered_posts_idでログイン中ユーザーが「投票した」ポストのidを呼び出している。
- Posw.where.not("id ~ で、投票したpost_id以外のidのポストの中から10個をランダムで返している。
users_controllerにnot_answered_postsを追加する
users_controller.rbdef not_answered_posts @user = current_api_v1_user answered_posts_id = "SELECT post_id from likes WHERE user_id = :user_id" not_answered_posts = Post.where.not("id IN (#{answered_posts_id})", user_id: @user.id).order('RANDOM()').limit(10) json_data = { 'posts': not_answered_posts, } render json: { status: 'SUCCESS', message: 'Loaded the not_answered_posts', data: json_data} endReact
Home.js
- CardのonSwipeLeft、onSwipeRightで左右にスワイプ時に呼び出す関数を指定している。
- submitSuki, submitKiraiで「好き」「嫌い」を投票している。
- 再読み込みボタンで、リロードし、追加のnot_answered_posts(ログイン中のユーザーがまだ投票していないポスト)を10個ずつ読み込んでいる。
Home.jsimport React from 'react'; import PropTypes from 'prop-types'; import './HomeStyles.css' import { withStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as actions from '../actions'; import queryString from 'query-string'; import _ from 'lodash'; import axios from 'axios'; import Cards, { Card } from 'react-swipe-card-chsstm'; import "normalize.css"; //import "./styles.css"; const styles = theme => ({ // ヘッダーロゴ homeimg: { height: '20%', width: '60%', display: 'block', margin: 'auto', }, conceptimg: { display: 'flex', width: '80%', display: 'block', margin: '10px auto', }, button: { margin: '0px 5px', }, sukibutton: { margin: '0px 15px', backgroudColor: '#000000', }, kiraibutton: { margin: '0px 15px', backgroudColor: '#ffffff' }, }); class Home extends React.Component { constructor(props) { super(props) this.state = { not_answered_posts: [] } const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid //新着順 axios.get(process.env.REACT_APP_API_URL + `/api/v1/not_answered_posts`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const data = response.data.data; this.setState({ not_answered_posts: data.posts, }); }) .catch(() => { }); this.reload = this.reload.bind(this); } reset = () => { this.setState(state => ({ id: state.id + 1 })); }; componentDidMount() { } submitSuki(post) { const { CurrentUserReducer } = this.props const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const data = { user_id: CurrentUserReducer.items.id, post_id: post.id, suki: 1, } axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(response => { }) .catch(error => { }) } submitKirai(post) { const { CurrentUserReducer } = this.props const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const data = { user_id: CurrentUserReducer.items.id, post_id: post.id, suki: 0, } axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(response => { }) .catch(error => { }) } reload() { const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid //新着順 axios.get(process.env.REACT_APP_API_URL + `/api/v1/not_answered_posts`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const data = response.data.data; var not_answered_posts = this.state.not_answered_posts data.posts.forEach(post => { not_answered_posts.push(post); }); this.setState({ not_answered_posts: not_answered_posts, }); }) .catch(() => { }); } render() { const { CurrentUserReducer } = this.props; const { classes } = this.props; let cards; return ( <div className="home"> <div className="background"> <Button size="large" variant="contained" color="blue" onClick={this.reload} className={classes.sukibutton}> 再読み込み </Button> </div> <Cards onEnd={this.endSwipe} className="master-root" likeOverlay={<h1>スキ</h1>} dislikeOverlay={<h1>キライ</h1>} ref={(ref) => cards = ref} > {this.state.not_answered_posts.map(item => <Card onSwipeLeft={() => this.submitKirai(item)} onSwipeRight={() => this.submitSuki(item)}> <h2>{item.content}</h2> </Card> )} </Cards> <div className="buttonArea"> <Button size="large" variant="contained" color="blue" onClick={() => { cards.dislike(); }} className={classes.sukibutton}> キライ </Button> <Button size="large" variant="contained" color="red" onClick={() => { cards.like(); }} className={classes.kiraibutton}> スキ </Button> </div> </div > ) } } Home.propTypes = { classes: PropTypes.object.isRequired, post: PropTypes.object.isRequired, }; const mapState = (state, ownProps) => ({ CurrentUserReducer: state.CurrentUserReducer, }); function mapDispatch(dispatch) { return { actions: bindActionCreators(actions, dispatch), }; } export default connect(mapState, mapDispatch)( withStyles(styles, { withTheme: true })(Home) );HomeStyles.css
- Home.jsのスタイルを別途HomeStyles.cssに記述しています。
- これ、結構うまくやらないとちゃんと配置してくれなくて苦労しました。Cardを10枚ずつ重ねて表示していて、z-indexを使っているから、ここで配置がずれたりする。
- ここを参考にしました。https://github.com/chsstm/react-swipe-card/blob/master/stories/style.css
HomeStyles.csshtml { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } body { margin: 0 auto; padding: 0; font-family: sans-serif; text-align: center; } img { width: 100%; } li { list-style: none; } h2 { text-align: left; word-wrap: break-word; margin: 20px; font-size: 20pt; } .home { position: relative; overflow: hidden; width: 100%; height: 100%; min-height: 500px; } .master-root { margin: 10px 0px; position: absolute; height: 50%; width: 100%; /* z-index:2; */ } .card { background-color: white; background-size: cover; position: absolute; left: 0; right:0; top:0; bottom:0; background: #f8f3f3; height: 80%; width: 80%; margin: auto; transition: box-shadow 0.3s; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); border: 10px solid #212121; cursor: pointer; } .buttonArea{ position: absolute; bottom: 20%; width: 100%; margin: 0 auto; } .animate { transition: transform 0.3s; box-shadow: none; } .inactive { box-shadow: none; } .alert { width: 45%; min-height: 10%; position: absolute; z-index: 9999; opacity: 0; transition: opacity 0.5s; color: white; vertical-align: middle; line-height: 3rem; } .alert-visible { opacity: 1; } .alert-right { top: 0; right: 0; background: red; border-top-left-radius: 50px; border-bottom-left-radius: 50px; } .alert-left { top: 0; left: 0; background: blue; border-top-right-radius: 50px; border-bottom-right-radius: 50px; } .alert-top { background: black; border-radius: 50px; transform: translate(-50%, 0); margin-left: 50%; } .alert-bottom { bottom: 0; background: black; border-top-left-radius: 50px; border-radius: 50px; transform: translate(-50%, 0); margin-left: 50%; } .action-button { cursor: pointer; padding: 10px 50px; border: none; outline: none; background-color: lightgrey; margin: 0px 10px; } .action-button:active { background-color: black; color: white; }
- 投稿日:2020-01-13T12:02:33+09:00
redux-formを使って投稿画面を実装する【初学者のReact×Railsアプリ開発 第6回】
やったこと
- redux-formを使ってReactで投稿画面を実装した
- axiosを使って、Railsで実装したAPIと連携させ、投稿できるようにした。
成果物
実装手順
モジュールのインストール
アプリ開発全体で使うモジュールはここでインストールしています。
App.js
App.jsimport Login from './containers/Create'; <Auth> <Switch> <Route exact path="/" component={Home} /> <Route path='/create' component={Create} /> </Switch> </Auth>reducers/rootReducer.js
- redux-formからformReducerを読み込んでいる。
rootReducer.jsimport { combineReducers } from 'redux' import { reducer as formReducer } from 'redux-form' import { routerReducer } from 'react-router-redux' import CurrentUserReducer from './CurrentUserReducer' const rootReducer = combineReducers({ CurrentUserReducer, form: formReducer, router: routerReducer, }) export default rootReducercontainers/Create.js
- これが投稿画面。
- フォームCreateForm.jsをインポートしている
- submitPostで送信の処理を書いている。
- フォーム内の改行などはreplaceなどを使って消去している
- axiosの使い方はここで書いている通りで、localStorageからauth_tokenなどを呼び出して、headerにつけて送っていることで認証できる。
- 送信後のフォームの処理は、本当はactionを経由しなければいけないのかな〜...(この実装方法だと、タイミングは微妙に変)
Create.jsimport React from 'react' import CreateForm from '../components/CreateForm' import axios from 'axios' import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as actions from '../actions'; class Create extends React.Component { constructor(props) { super() this.state = { postSuccess: false, } } submit = values => { const data = { content: values.notes, user_id: 1 } axios.post(process.env.REACT_APP_API_URL + '/api/v1/posts', data) } submitPost = values => { const { CurrentUserReducer } = this.props; var value_content = values.notes var send_content = value_content.replace(/\r?\n/g, ""); const data = { content: send_content, user_id: CurrentUserReducer.items.id } const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid axios.post(process.env.REACT_APP_API_URL + '/api/v1/posts', data, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(() => { }) .catch(() => { values.notes = value_content }) values.notes = "" } render() { const { actions } = this.props; const { CurrentUserReducer } = this.props; const { classes } = this.props; return ( <div> <h3>テーマを投稿する</h3> <CreateForm onSubmit={this.submitPost} /> </div> ) } } const mapState = (state, ownProps) => ({ CurrentUserReducer: state.CurrentUserReducer }); function mapDispatch(dispatch) { return { actions: bindActionCreators(actions, dispatch), }; } export default connect(mapState, mapDispatch)(Create);components/CreateForm.js
- validateで必須項目や50字以内にするように指定している。
- const CreateForm内のFieldタグで色々オプションを付けている。multilineは複数行のフォームにするかどうか?で、rowsMaxで最大の行数を指定できる。Enter押すと自動で行数が増えていく。
CreateForm.jsimport React from 'react' import { Field, reduxForm } from 'redux-form' import TextField from '@material-ui/core/TextField' import Button from '@material-ui/core/Button'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles(theme => ({ button: { margin: theme.spacing(1), }, field: { width: '80%', height: '200px', } })); const validate = values => { const errors = {} const requiredFields = [ 'notes' ] requiredFields.forEach(field => { if (!values[field]) { errors[field] = 'Required' } }) if ( values.notes && values.notes.length > 51 ) { errors.notes = '50字以内にしてください' } return errors } const renderTextField = ({ label, input, meta: { touched, invalid, error }, rows = 3, ...custom }) => ( <TextField label={label} placeholder={label} error={touched && invalid} helperText={touched && error} {...input} rows={rows} {...custom} variant="outlined" /> ) const CreateForm = props => { const { handleSubmit, pristine, reset, submitting } = props const classes = useStyles(); return ( <form onSubmit={handleSubmit}> <div> <Field name="notes" component={renderTextField} className={classes.field} label="" multiline rowsMax="7" margin="normal" /> </div> <div> <Button variant="contained" color="primary" type="submit" className={classes.button} //endIcon={<Icon>send</Icon>} > Send </Button> </div> </form> ) } export default reduxForm({ form: 'CreateForm', // a unique identifier for this form validate, })(CreateForm)
- 投稿日:2020-01-13T12:02:33+09:00
redux-formを使って投稿画面を実装する【初学者のReact×Railsアプリ開発 第7回】
やったこと
- redux-formを使ってReactで投稿画面を実装した
- axiosを使って、Railsで実装したAPIと連携させ、投稿できるようにした。
成果物
実装手順
モジュールのインストール
アプリ開発全体で使うモジュールはここでインストールしています。
App.js
App.jsimport Login from './containers/Create'; <Auth> <Switch> <Route exact path="/" component={Home} /> <Route path='/create' component={Create} /> </Switch> </Auth>reducers/rootReducer.js
- redux-formからformReducerを読み込んでいる。
rootReducer.jsimport { combineReducers } from 'redux' import { reducer as formReducer } from 'redux-form' import { routerReducer } from 'react-router-redux' import CurrentUserReducer from './CurrentUserReducer' const rootReducer = combineReducers({ CurrentUserReducer, form: formReducer, router: routerReducer, }) export default rootReducercontainers/Create.js
- これが投稿画面。
- フォームCreateForm.jsをインポートしている
- submitPostで送信の処理を書いている。
- フォーム内の改行などはreplaceなどを使って消去している
- axiosの使い方はここで書いている通りで、localStorageからauth_tokenなどを呼び出して、headerにつけて送っていることで認証できる。
- 送信後のフォームの処理は、本当はactionを経由しなければいけないのかな〜...(この実装方法だと、タイミングは微妙に変)
Create.jsimport React from 'react' import CreateForm from '../components/CreateForm' import axios from 'axios' import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as actions from '../actions'; class Create extends React.Component { constructor(props) { super() this.state = { postSuccess: false, } } submit = values => { const data = { content: values.notes, user_id: 1 } axios.post(process.env.REACT_APP_API_URL + '/api/v1/posts', data) } submitPost = values => { const { CurrentUserReducer } = this.props; var value_content = values.notes var send_content = value_content.replace(/\r?\n/g, ""); const data = { content: send_content, user_id: CurrentUserReducer.items.id } const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid axios.post(process.env.REACT_APP_API_URL + '/api/v1/posts', data, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(() => { }) .catch(() => { values.notes = value_content }) values.notes = "" } render() { const { actions } = this.props; const { CurrentUserReducer } = this.props; const { classes } = this.props; return ( <div> <h3>テーマを投稿する</h3> <CreateForm onSubmit={this.submitPost} /> </div> ) } } const mapState = (state, ownProps) => ({ CurrentUserReducer: state.CurrentUserReducer }); function mapDispatch(dispatch) { return { actions: bindActionCreators(actions, dispatch), }; } export default connect(mapState, mapDispatch)(Create);components/CreateForm.js
- validateで必須項目や50字以内にするように指定している。
- const CreateForm内のFieldタグで色々オプションを付けている。multilineは複数行のフォームにするかどうか?で、rowsMaxで最大の行数を指定できる。Enter押すと自動で行数が増えていく。
CreateForm.jsimport React from 'react' import { Field, reduxForm } from 'redux-form' import TextField from '@material-ui/core/TextField' import Button from '@material-ui/core/Button'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles(theme => ({ button: { margin: theme.spacing(1), }, field: { width: '80%', height: '200px', } })); const validate = values => { const errors = {} const requiredFields = [ 'notes' ] requiredFields.forEach(field => { if (!values[field]) { errors[field] = 'Required' } }) if ( values.notes && values.notes.length > 51 ) { errors.notes = '50字以内にしてください' } return errors } const renderTextField = ({ label, input, meta: { touched, invalid, error }, rows = 3, ...custom }) => ( <TextField label={label} placeholder={label} error={touched && invalid} helperText={touched && error} {...input} rows={rows} {...custom} variant="outlined" /> ) const CreateForm = props => { const { handleSubmit, pristine, reset, submitting } = props const classes = useStyles(); return ( <form onSubmit={handleSubmit}> <div> <Field name="notes" component={renderTextField} className={classes.field} label="" multiline rowsMax="7" margin="normal" /> </div> <div> <Button variant="contained" color="primary" type="submit" className={classes.button} //endIcon={<Icon>send</Icon>} > Send </Button> </div> </form> ) } export default reduxForm({ form: 'CreateForm', // a unique identifier for this form validate, })(CreateForm)
- 投稿日:2020-01-13T06:32:47+09:00
Reactで作成した静的webアプリをサブドメインを用いてapacheにデプロイするまで
はじめに
今日,AWSとかAzureとか,webサービスをデプロイするのにクラウドサービスが流行っていますが,僕は勉強も兼ねてVPSを使っているのでapacheにデプロイする方法を書こうかと思います.
時代遅れな記事ですみません...
ReactAppをビルド
以下のコマンドでビルドします.
npm run buildルートディレクトリにbuildフォルダが出来上がると思うので,
scp
かなんかを使ってVPSに転送します.
転送先はとりあえずどこでもいいです.サブドメインの作成
もともとブログも管理していたということもあって,今回はサブドメインを作ろうかと思いました.
サブドメインの作り方はレンタル先によると思いますが,僕はConohaVPSしかわからないのでそれについて説明します.
ここで,メインドメインは取得して登録済みであることは前提とします.
- ログインする
- 左の欄から「DNS」をクリックする
- メインドメインをクリックする
- 右上の方にある編集ボタンをクリックする
- 追加ボタンをクリックし,タイプ「A」,名称「<サブドメインにしたいワード>」,TTL「<キャッシュ期間>」,値「<サーバのIP>」をれぞれ入力する
- 保存する
Conoha側の設定は以上です.
Apacheの設定
ssh
とかでターミナルからサーバ内に接続します.# httpdのconf.dディレクトリに移動 cd /etc/httpd/conf.d # virtual.confを作成(.conf拡張子であれば名前は何でも大丈夫です) vim virtual.confvirtual.confには以下の内容を記述します.
<VirtualHost *:80> ServerAdmin root@localhost ServerName <サブドメイン> DocumentRoot <buildファイルまでのパス> <Directory "<DocumentRootと同じパス>"> Options Indexes FollowSymLinks AllowOverride None Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.html [QSA,L] Require all granted </Directory> CustomLog /etc/httpd/logs/hoge_access_log common ErrorLog /etc/httpd/logs/hoge_error_log </VirtualHost *:80>
DocumentRoot
ですが,scp
したときのbuildファイルまでのパスを指定します.
基本的には/var/www/html/
配下とかが良いかと思います.もちろん,応用してSSLもできます.
そのときは,<VirtualHost *:443>
を追加して,その中に証明書のkeyとか中間証明書とかをSSLCertificateFile
とかSSLCertificateKeyFile
とかに指定する必要があります.mod_ssl
も必要です.あと,もともと運営しているサイトが合ったとしたら,そのドメインもこの際に
<VirtualHost *:80>
内に定義してしまったほうが良さそうです.
httpd.conf
内にDocumentRoot
を記述しちゃうと,メインドメインに直で飛ばされてしまうということが多々ありました...動作確認
httpdをリロードします.
systemctl restart httpdこれでアクセスしてみて確認します.
ハマったこと
apacheの設定で結構ハマってしまいました.
httpd.conf
とかデフォルトのまま使ったり,色々なモジュールをインストールしていると,自動生成されるファイルのせいでうまく行かなかったりと色々大変でした...
やっぱりデフォルトのファイルはバックアップしといて,自分で1つ1つ調べながら設定ファイルも1から作るのが手っ取り早いですね.
僕はまだ全部勉強しきれていないですが,これから頑張ろうかと思います.
- 投稿日:2020-01-13T00:56:06+09:00
「環境構築メモ」 create-react-app
自分への備忘録
Windows10のVagrant環境下でCentOS7を立ち上げて、create-react-appを実行できるようにします。
Vagrant initが実行できる状態から始めます。VagrantでCentOS7をインストールする。
$ vagrant init centos/7CentOS7を立ち上げて接続する。
(vagrantコマンドで接続する場合)
$ vagrant up $ vagrant sshvimをインストールする。
$ sudo yum -y install git gcc ncurses-devel $ git clone https://github.com/vim/vim.git $ cd vim/src $ make $ sudo make installnvmとnodejsをインストールする。
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash $ source ~/.bashrc $ command -v nvm nvm $ nvm install --lts $ node --versionyarnをインストールする。
$ curl -o- -L https://yarnpkg.com/install.sh | bash $ source ~/.bashrc $ yarn --versiongitのインストール
$ sudo yum remove git $ sudo yum -y install wget $ sudo wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.5.tar.gz $ sudo tar xzvf git-2.9.5.tar.gz $ sudo yum -y install openssl-devel curl-devel expat-devel perl-CPAN $ cd git-2.9.5 $ sudo make prefix=/usr all $ sudo make prefix=/usr install $ git --version(Option)BashプロンプトにGitのブランチを表示する。
git-prompt.shの場所を確認する。
$ find ~/ -name git-prompt.sh /home/vagrant/git-2.9.5/contrib/completion/git-prompt.shプロンプトの設定を.bashrcに追加する。
~/.bashrcsource /home/vagrant/git-2.9.5/contrib/completion/git-prompt.sh PS1="\u@\h:\W$(__git_ps1)\$ ".bashrcの設定をBashに反映する。
~/.bashrc$ source ~/.bashrccreate-react-app
$ mkdir -p ~/environment/react-app $ cd ~/environment/react-app $ npx create-react-app my-app $ cd my-app
- 投稿日:2020-01-13T00:34:43+09:00
Form要素以外をクリックした時にイベントを発火させる方法
環境
- react:v16.12.0
- コマンドラインツール:create-react-app
完成形
実装手順
onChangeイベントで値が更新されるFormが実装されているアプリに実装する順序を説明します。
body全体にクリックイベントにより発火する関数の設定
CommentComponentがunmountする時に、イベントを削除する。
Comment.jsxuseEffect(() => { if(isActive) document.body.addEventListener("click", e => handleClick(e)); return () => { document.body.removeEventListener("click", e => handleClick(e)); }; }, [isActive]); /* isActiveは編集中orテキストを表示しているかの状態を管理している true : 編集中 false : 表示のみ */クリックイベントにより発火する関数の定義
ここで注意するべきは
if文
で処理を飛ばしている箇所です。Comment入力部分(textarea)と保存を完了する部分(button)がクリックされた時はすでに別の処理が割り当てられているのでこの関数を実行してはいけません。
よって関数では引数にイベントを渡し、クリックしたDOMがe.targetそれ以外であるか判定します。
それ以外であるならば状態をテキストの表示のみに戻します。Comment.jsx/* * textArea.current ... Comment入力部分(textarea)のDOMの参照 * submitBtn.current ... Commentを保存する部分(button)のDOMの参照 */ const handleClick = e => { if (textArea.current === e.target || submitBtn.current === e.target) return; toggleActive(false); };Comment関連部分の参照を保持する
ReactHooksのuseRefを利用する。
Comment入力部分(textarea)と保存を完了する部分(button)の参照を得ることができ、上記の条件分岐を利用することが出来る。
Comment.jsxconst textArea = useRef(null); const submitBtn = useRef(null); <form> <textarea ref={textArea} /> <br /> <button type="submit" ref={submitBtn} > 保存 </button> </form>完成
元のForm
https://github.com/taiki-fw/ReactHooksForm/tree/master完成後のForm
https://github.com/taiki-fw/ReactHooksForm/tree/1feat/FireEventあとがき
今回の場合だとFormの値保存を別箇所で行なっているので特に問題ではないのですが、ReduxなどのActionなどを利用される際は、stateが変更されるまでの遅延に注意して頂きたいです。
実際にやらかしました...DOMに直接Clickイベントで発火する関数を設定しているのであまり推奨はされないかもしれないです。
参考程度になれば幸いです。
また、間違いなどあれば指摘して頂けると幸いです。