20190819のReactに関する記事は6件です。

一番簡単なreact-reduxアプリケーション

初めに

モダンなフロントエンド開発だと結構必須になってきているreact-redux
しかしながら、reactだけでも慣れが必要なのにそれに加えてreduxとなると余計混乱します。
僕はめっちゃ混乱しました。チュートリアルをやってみてもなかなか理解が深まらず、使えぬまま放置してしまったことがあります。

そういう人も多からずいらっしゃると思うのでそんな方や、reduxよくわかんないけどともかく慣れたいという人のために、今回はものすごく簡単なアプリケーションを作成することによって、もっとも簡単なreduxに触れてみたいと思います。reduxのハードルが少しでも下がれば幸いです。

これぐらいの大きさのアプリケーションだと正直reduxを実装する意味は皆無ですが練習です!

※図を使った細かなデータの行き来は他の方が詳しく解説されているので、今回はそういったものは用意しておりませんのでご了承ください

作成目標

+を押したら数字が増えて
-を押したら数字が減る
そんな簡単なアプリケーション
herokuでdeployしてます
https://easiest-react-redux-app.herokuapp.com/
作成したコードはgitにあげております(デプロイに際してコードが少し変化してます)
https://github.com/keyyang0723/easiest-react-redux

環境構築

reactをやるうえで最も簡単な
create-react-appを使います

$npm install -g create-react-app
$create-react-app easiest-react-redux

これでアプリケーションが作成されます。
ここからはコンソールコマンドは基本的にアプリのホームディレクトリで行っていると思ってください

$npm run start

でReactのマークが回転していればOK

reduxの導入

$npm install react-redux -s
$npm install redux -s
不要ファイルの削除

次に不要なファイルを削除します
srcの中身はIndex.js,App.js以外は不要なので任意の方法で削除してください。

必要なフォルダの作成

必要なフォルダを作成します。
それぞれreduxで使用する
actions
reducers
components
containers
をsrc直下に作成します。
この時ついでにApp.jsをcomponents直下に移動させておきます。

$mkdir src/actions
$mkdir src/reducers
$mkdir src/components
$mkdir src/containers
$mv src/App.js src/components/App.js

これで作成の準備ができました。お疲れ様です。

実装

Viewの作成

まずはひとまずreduxを無視してreactを使いviewを作成しましょう

index.js
import React from "react";
import { render } from "react-dom";
import App from './components/App';

const rootElement = document.getElementById("root");
render(<App />, rootElement);
App.js
import React,{ Component } from "react";

class App extends Component{
render(){
  return (
    <div>
      <p>0</p>
      <button>+</button>
      <button>-</button>
    </div>
  );}   
}

export default App

なんの変哲もないreactのコンポーネントです。

$npm run start

すれば表示はされますが、もちろん値の更新などは行われません。

従来のreactであればここにApp.jsのstateを使い,buttonにイベントを用意してそれらを着火させstateを更新していきます。(このアプリケーションはそれだけの機能なので従来ならばそのやり方でOKですが今回は特別です)
ではここからはreduxを実際に利用していきましょう。

reduxの導入

storeの作成を実装する

まずはデータの置き場であるstoreを初期化する部分を作成していきましょう。
storeを通じて画面を表示させるためにはactions=>reducersのデータの流れが作成されていないとエラーが出てしまうので、エラーが出ない程度に実装します。

actionsの作成

まずはactionを作成します。actionですが今回のアプリケーションは数字の増加と減少二つの機能があるので両方追加しておきます。
ここではaction typeの指定とdispatchされてきた初期値の受け渡しのみ実装しておきます(typeを指定しないと怒られる)

actions/index.js
/*actionの作成*/
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export const increment = (number) => {
  return {
      type: INCREMENT,
      number
  };
};

export const decrement = () => {
  return {
  };
};

またtypeは文字列のまま使うこともできますが、constで定義しておくのがイカした書き方のようです。

reducersの作成

次にreducerの作成です。
従来ならばactionsから送られてきたtypeによって分岐分けし、データ作成の機能を作成しますが、今回は表示だけなので受け取った値をそのまま渡します。

reducers/index.js
/*reducerの作成*/
const reducer = (state, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

export default reducer

createStoreでstoreの初期化の実装

storeを作成するためにreduxのcreateStore()を使用します。こちらはreducerを引数に渡す必要があるので、import作成します。

index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from './components/App';
import reducer from './reducers/index';

const store = createStore(reducer);

const rootElement = document.getElementById("root");
render(
  <Provider store={store}>
    <App />
  </Provider>
, rootElement);

またこの際初期値の挿入のため普段はここには配置しないactionsを配置しています。後で消します

react-reduxのProviderを利用を利用しここにラップされているコンポーネントすべてからstoreの値を参照できるようにします。

これで問題がなければ

$npm run start

をたたけば先ほど作成したviewと同じ画面が表示されます。
これでstoreを作成することに成功しました!
ここから、actionとreducerを実装していき、console.logを使い正しいデータの流れができているか確認します。

actionの実装

actionはdispatchから送られてくるstateを受け取り、そこから値を抽出してreducerに送ります。ここでは値に触りません。

actions/index.js
/*actionの作成*/
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export const increment = state => {
  return {
    type: INCREMENT,
    number: state.number
  };
};

export const decrement = state => {
  return {
    type: DECREMENT,
    number: state.number
  };
};

rudexの書式に沿っていればこれだけ書くだけで自動的に値をreducerに送ってくれます。

reducerの実装

次にreducerを実装してみます。

reducer/indexl.js
/*reducerの作成*/
const reducer = (state, action) => {
console.log(action)//あとでデータの確認に使います。本来ならば不要です。
  switch (action.type) {
    case "INCREMENT":
      return {
        number: action.number + 1
      };
    case "DECREMENT":
      return {
        number: action.number - 1
      };
    default:
      return state;
  }
};

export default reducer;

reducerはactionsから流れてきたstate, actionを受けとり値を変更しstoreを更新します。
今回では+を押したときにINCREMENTで値を+1し、-を押したときにDECREMENTで値を-1します。

これでいったんactionsとreducersの作成が終わりました。この二つによってstoreがうまく変更されるかを確認してみましょう。

index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from './components/App';
import reducer from './reducers/index';
import { increment, decrement } from './actions/index';

const store = createStore(reducer);
store.dispatch(increment({number: 0}))//テスト用コードです
store.dispatch(decrement({number: 0}))//テスト用コードです


const rootElement = document.getElementById("root");
render(
  <Provider store={store}>
    <App />
  </Provider>
, rootElement);

今回はデータの確認だけですのでindex.jsに直接actionsをimportし先ほどreducersに用意していたconsole.logにデータが正しく表示されているかを確認しましょう。

store.dispatch(increment({number: 0}));
//store.dispatch(decrement({number: 0}));

のようにコメントアウトを切り替えることで値がそれぞれ
{type: "INCREMENT", number: 0}
{type: "DECREMENT", number: 0}
のふたつに切り替わっていれば無事実装されています。

確認ができたらreducers/index.jsとindex.jsにあるテストようのコードを削除しておきましょう。

おめでとうございます!これであなたはstore, actions, reducersを用いてデータの流れを作成することに成功しました!あとはこれらを着火するイベントと変更されてた値を表示する機能を作成すれば完成です!

componetsの作成

さて、いよいよ今まで作成したstore, actions, reducersを用いて画面を作成していきましょう。
storeで値を保持してそれらを表示させ、イベントなどでactionに値とaction typeを流し込みreducerでstoreを更新するというサイクルを作成します。

mapStateToPropsとmapDispatchToPropsに関して

ここで新たにmapStateToPropsとmapDispatchToPropsというreduxで用いられる関数が登場します。
個人的にここが結構ややこしいと思うので
今回では
mapStateToPropsはstoreの値を監視しそれらを適切な形に変化させて、表示させるもの
mapDispatchToPropsはstoreの値を変化させるためのイベントを宣言するもの
くらいに考えておいてもらってよいと思います。
特に後者はdispatch(increment({ number: number }));などという見慣れないものが現れますがdispatch()のカッコ内に記載したactionsの関数に値を送りこむものくらいで結構です。

componets部分の変更

まずはApp.jsを少し書き換えます、今回はClassである必要はないので

const  App = () =>(
    <React.Fragment>
      <p>0</p>
      <button>+</button>
      <button>-</button>
    </React.Fragment>
  )

のように書き換えます

mapStateToPropsを使ってstoreの値を表示する

そしてこのAppにstoreからの値を流し込むためにmapStateToPropsを記入し、react-reduxのconnectを用いてAppをとつないでやります。

App.js
import React from "react";
import { connect } from "react-redux";

const  App = (state) =>(
    <React.Fragment>
      {console.log(state)}//テスト用です。
      <p>{state.number}</p>
      <button>+</button>
      <button>-</button>
    </React.Fragment>
  )

function mapStateToProps(state) {
  return {
    number: state ? state.number : 0
  };
}

export default connect(
  mapStateToProps,
  null
)(App);

このような感じです。今回の場合、初期表示時のためstoreから流れてくるstateはnullですが、

number: state ? state.number : 0

によって0に変えています
コンソールに{number: 0, dispatch: ƒ}が来ていれば問題なくいっています。
さぁ、これでstoreの値をviewで表示することができました!!
あとはstoreの値を変更するイベントをmapDispatchToPropsを用いて実装すれば完成です!

mapDispatchToPropsを実装する

mapDispatchToPropsではstateの状態を変更するイベントを作成しAppに渡してやります。そしてApp内でそれを着火することでstateの変更を行います。
まずは+のボタンのイベントから実装していきます。

function mapDispatchToProps(dispatch){
    return {
      onClick(number) {
        dispatch(increment({ number: number }));
      }
    }
}

関数として表示されている数字のnumberを受け取りincrementにdispatchしてやるだけです。

components/App.js
import React from "react";
import { connect } from "react-redux";
import { increment } from '../actions/index';

const  App = (state) =>(
    <React.Fragment>
      {console.log(state)}
      <p>{state.number}</p>
      <button>+</button>
      <button>-</button>
    </React.Fragment>
  )

function mapStateToProps(state) {
  return {
    number: state ? state.number : 0
  };
}

function mapDispatchToProps(dispatch){
    return {
      onClick(number) {
        dispatch(increment({ number: number }));
      }
    }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

最後にconnectでAppに忘れずにつないでやります。
これでconsole.logを見てやると、stateの中のdispatchに記載したonClickが表記されています。後はdecrement部分も実装しのonClickにわたしてやればOKです。

<button onClick={() => {state.onClick(state.number,INCREMENT)}}>+</button>

これで+ボタンの実装が完了しました。同様に―ボタンを作成すれば機能の実装は完了です!!
おめでとうございます!!これでかなり小さいものですがreact-reduxアプリケーションが完成しました!!
最後に今回は小さいアプリケーションなので分けずに書いてしまっている部分を分断していきます。

containersの作成

components/App.jsに直接書いてしまっているactionへの値のやり取りやstoreからの値の取り込みを行う部分をcontainers/dispayNumber.jsとして作成していきます。また今回は一機能なので必要ないように感じますが
機能が増えたときにcomponents/App.jsでまとめてからindex.jsに渡したいので、components/displayNumber.jsを作成しcomponents/App.jsでimportしてやります

components/App.js
import React from "react";
import DisplayNumber from "../containers/displayNumber";

export default function App(){
  return(
    <React.Fragment>
      <DisplayNumber />
    </React.Fragment>
  )
}
components/Counter.js
import React from 'react';
import { INCREMENT, DECREMENT } from "../actions/index";

const  Counter = (state) =>(
  <React.Fragment>
    <p>{state.number}</p>
    <button onClick={() => {state.onClick(state.number,INCREMENT)}}>+</button>
    <button onClick={() => {state.onClick(state.number,DECREMENT)}}>-</button>
  </React.Fragment>
)

export default Counter
containers/dispayNumber.js
import { connect } from "react-redux";
import Counter from "../components/Counter";
import { INCREMENT, increment, DECREMENT, decrement } from "../actions/index";

function mapStateToProps(state) {
  return {
    number: state ? state.number : 0
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onClick(number, types) {
      switch (types) {
        case INCREMENT:
          dispatch(increment({ number: number }));
          break;
        case DECREMENT:
          dispatch(decrement({ number: number }));
          break;
        default:
      }
    }
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);

お疲れ様です!これで形も整えることができ無事react-reduxアプリケーションが完成しました!!

今回は非常に機能の少ないアプリケーションであり、まだまだ使用していない機能もたくさんありますが、redux理解への第一歩としてお役に立っていれば幸いです。

初期値の追加

この状態だと初期stateが導入されておらず、初期表示を行いたいときに若干不都合でした

index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from './components/App';
import reducer from './reducers/index';

const store = createStore(reducer, initialState);

const rootElement = document.getElementById("root");
render(
  <Provider store={store}>
    <App />
  </Provider>
, rootElement);

const initialState = {
  number: 0
};
const store = createStore(reducer,initialState);
const initialState = {
  number: 0
};

を加え初期stateを追加してやりましょう。

参照

デプロイの時お世話になりました。
https://qiita.com/kuniken/items/70c2b5cd77d7c9301bcf#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%BD%E3%83%BC%E3%82%B9

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npx create-react-app my-app --typescriptのあと

npx create-react-app my-app --typescript

# りんと
npm install --save tslint
# スタイル
npm install --save styled-components @types/styled-components

# air-bnbのりんとやりたい
npm install --save-dev tslint-config-airbnb
tslint.json
{
    "extends": "tslint-config-airbnb",
}

airbnbのりんとのめんどいやつはfalseにしたいので

tslint.json
{
    "extends": "tslint-config-airbnb",
    // 追加
    "rules": {
        "import-name": false,
        "variable-name": false,
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-hooksでreduxを使う

react-hooksでreduxを使う

案件でreactの改修が起きそうなので、react,redux,typescript,react-routerで気持ちよく書きたいと思い殴り書きだがサンプルを作って試してみました。

作ったリポジトリ

https://github.com/numa999/react-redux-example

ざっくり解説

router.tsx
import React from 'react'
import { BrowserRouter, Route, Link } from 'react-router-dom'

import App from './App'
import Counter from './counter'

const Router: React.FC = () => (
  <BrowserRouter>
    <nav>
      <ul>
        <li><Link to="/">HOME</Link></li>
        <li><Link to="/counter">COUNTER</Link></li>
      </ul>
    </nav>
    <hr/>
    <div className="content">
      <Route exact path="/" component={ App } />
      <Route path="/counter" component={ Counter } />
    </div>
  </BrowserRouter>
)

export default Router

ここは特に工夫していないです。ナビゲーションも適当。

counter.tsx
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { RootState, incrementActionCreator, decrementActionCreator } from './store';

const Counter: React.FC = () => {
  const [count, setCount] = useState(0)
  const dispatch = useDispatch()
  const counterState = useSelector((state: RootState) => state.counter)

  return (
    <div>
      react hooks count { count }
      <div>
        <button onClick={() => setCount(count + 1)}>+</button>
        <button onClick={() => setCount(count - 1)}>-</button>
      </div>
      redux count { counterState.count }
      <div>
        <button onClick={() => dispatch(incrementActionCreator())}>+</button>
        <button onClick={() => dispatch(decrementActionCreator())}>-</button>
      </div>
    </div>
  )
}

export default Counter

react-hooksもそんな使ってないのでちょっと余計なコードも追加。

useSelector

前までの mapStateToProps の代わり
https://react-redux.js.org/next/api/hooks#useselector

useDispatch

storeのdispatchを生成してくれる。アクションの中身を指定して渡してあげる。useCallbackは一旦省略。
https://react-redux.js.org/next/api/hooks#usedispatch

store

store.ts
import { combineReducers, createStore } from 'redux'

// Counter State
interface CounterState {
  count: number
}

const counterInitialState: CounterState = {
  count: 0
}

// actions
export const INCREMENT = 'COUNTER/INCREMENT'
export const DECREMENT = 'COUNTER/DECREMENT'

interface IncrementAction {
  type: typeof INCREMENT
  payload: null
}

interface DecrementAction {
  type: typeof DECREMENT
  payload: null
}

type CounterActionTypes = IncrementAction | DecrementAction

export const incrementActionCreator = (): CounterActionTypes => {
  return {
    type: INCREMENT,
    payload: null
  }
}

export const decrementActionCreator = (): CounterActionTypes => {
  return {
    type: DECREMENT,
    payload: null
  }
}
//---

// reducers
export const counterReducer = (
  state = counterInitialState,
  action: CounterActionTypes
): CounterState => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 }
    case DECREMENT:
      return { count: state.count - 1 }
    default:
      return state
  }
}
//---

// combine
export const rootReducer = combineReducers({
  counter: counterReducer
})

export type RootState = ReturnType<typeof rootReducer>

export const store = createStore(rootReducer)

ReturnType 知らなかった。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#example-4

感想

ここにreset用のアクションとか付け足したいけど省略。最近vuexばっかり使っていたので、比較でもないですが僕はやっぱりreactの方が好きだなと思った。

参考

https://react-redux.js.org/next/api/hooks
https://redux.js.org/recipes/usage-with-typescript
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html
https://qiita.com/seya/items/8291f53576097fc1c52a

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material UI の Shadow を完全にオフる方法(React)

はじめに

Button をオーバーライドして

shadow = 'none'

にしておけば、オフにできるかなと思ったけど
押下した時に現れる影は消せていないことが判明したので、ちょこっと付け加えることにした

押下前

image.png

押下時

image.png

解決

これだけ
none の付け方に注目

import {createMuiTheme, makeStyles} from '@material-ui/core/styles';
import {MuiThemeProvider} from "@material-ui/core";

const theme = createMuiTheme({
  shadows: ["none"]
});

const Index = () => {
  return(
    <MuiThemeProvider theme={theme}>
      <Button variant={valiant} className={classes.button}>
        ボタンだよん    
      </Button>
    </MuiThemeProvider>
  )
}

最後に

「この画面では、俺はリプルエフェクトだけが欲しいんだ!!!影はいらん!!」
という方は試してみてくださいな

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + material-UIでwebアプリ開発練習

あくまで個人のメモです。

Reactとmaterial-UIを使ったwebアプリ練習

Live Chat App with React Tutorial | React Hooks, Material UI, Socket.io, Node
を参考にしています(YouTube)。
というか、ここに書かれたコードをこの動画で説明されています。

準備

$ npx create-react-app mui-test
$ npm install material-ui
$ npm install socket
$ npm install express

コード

mui-test/public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8" />
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta
     name="description"
     content="Web site created using create-react-app"
   />
   <link rel="apple-touch-icon" href="logo192.png" />
   <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
   <title>React App with Material-UI</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
mui-test/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
src/index.css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}
src/App.js
import React from 'react';
import './App.css';

import Dashboard from './Dashboard';
import Store from './Store';

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <Store>
          <Dashboard />
        </Store>
      </div>
    );
  }
}

export default App;
src/App.css
.App {
  text-align: center;
}
src/Dashboard.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Chip from '@material-ui/core/Chip';
//import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';

import { CTX } from './Store';

const useStyles = makeStyles(theme => ({
  root: {
    margin: '50px',
    padding: theme.spacing(3, 2),
  },
  flex: {
    display: 'flex',
    alignItems: 'center',
  },
  topicsWindow: {
    width: '30%',
    height: '300px',
    borderRight: '1px solid grey',
  },
  chatWindow: {
    width: '70%',
    height: '300px',
    padding: '20px',
  },
  topicBox: {
    width: '30%',
    padding: '10px',
  },
  topicButton: {
    width: '15%',
  },
  userBox: {
    width: '20%',
    padding: '10px',
  },
  chatBox: {
    width: '65%',
    padding: '10px',
  },
  button: {
    width: '15%',
  },
}));

function Dashboard() {

  const classes = useStyles();

  // CTX store
  const {allChats, sendChatAction, addTopicAction} = React.useContext(CTX);
  const topics = Object.keys(allChats);

  // local state
  const [activeTopic, changeActiveTopic] = React.useState(topics[0]);
  const [textValue, changeTextValue] = React.useState('');
  const [userValue, changeUserValue] = React.useState('');
  const [addTopicValue, changeAddTopicValue] = React.useState('');

  return (
    <div>
      <Paper className={classes.root}>
        <Typography variant="h4" component="h4">
          メッセージアプリ
        </Typography>
        <Typography variant="h5" component="h5">
          {activeTopic}
        </Typography>
        <div className={classes.flex}>
          <div className={classes.topicsWindow}>
            <List>
              {
                topics.map(topic => (
                  <ListItem 
                    onClick={e => changeActiveTopic(e.target.innerText)}
                    key={topic} 
                    button
                  >
                    <ListItemText primary={topic} />
                  </ListItem>
                ))
              }
            </List>
          </div>
          <div className={classes.chatWindow}>
            {
              allChats[activeTopic].map((chat, i) => (
                <div className={classes.flex} key={i}>
                  <Chip
                    label={chat.from} 
                    className={classes.chip} 
                  />
                  <Typography 
                    variant="body1"
                    gutterBottom
                  >
                    {chat.msg}
                  </Typography>
                </div>
              ))
            }
          </div>
        </div>
        <div className={classes.flex}>
          <TextField
            label="add topic"
            className={classes.topicBox}
            value={addTopicValue}
            onChange={e => changeAddTopicValue(e.target.value)}
          />
          <Button
            variant="contained"
            color="primary"
            className={classes.topicButton}
            onClick={() => {
              addTopicAction(addTopicValue);
              changeAddTopicValue('');
            }}
          >
            add
          </Button>
        </div>
        <div className={classes.flex}>
          <TextField
            label="user name"
            className={classes.userBox}
            value={userValue}
            onChange={e => changeUserValue(e.target.value)}
          />
          <TextField
            label="Send a chat"
            className={classes.chatBox}
            value={textValue}
            onChange={e => changeTextValue(e.target.value)}
          />
          <Button 
            variant="contained"
            color="primary"
            className={classes.button}
            onClick={() => {
              sendChatAction({
                from: userValue, 
                msg: textValue, 
                topic: activeTopic
              });
              changeTextValue('');
              changeUserValue('');
            }}
          >
            Send
          </Button>
        </div>
      </Paper>
    </div>
  );
}

export default Dashboard;
src/Store.js
import React from 'react';
import io from 'socket.io-client';

export const CTX = React.createContext();

/*
  msg: {
    from: 'user',
    msg: 'message',
    topic: 'general'
  },

  state: {
    topic1: [msg, msg, msg, ...],
    topic2: [msg, msg, ...],
  }
*/


const initState = {
  general: [
  ],
}

function reducer(state, action) {
  const {from, msg, topic} = action.payload;

  switch(action.type) {
    case 'RECEIVE_MESSAGE': 
      return {
        ...state,
        [topic]: [
          ...state[topic],
          {from, msg, }
        ]
      }
    case 'ADD_TOPIC': 
      return {
        ...state,
        [topic]: [],
      }
    default:
      return state;
  }
}

let socket;

function sendChatAction(value) {
  socket.emit('chat message', value);
}

function addTopicAction(value) {
  socket.emit('add topic', value);
}

function Store(props) {

  const [allChats, dispatch] = React.useReducer(reducer, initState);

  if(!socket) {
    socket = io(':3001');
    socket.on('chat message', msg => {
      dispatch({type: 'RECEIVE_MESSAGE', payload: msg});
    });
    socket.on('add topic', topic => {
      if (!allChats[topic]) {
        dispatch({type: 'ADD_TOPIC', payload: {from:'', msg:'', topic}});
      }
    })
  }

  return (
    <CTX.Provider value={{allChats, sendChatAction, addTopicAction}}>
      {props.children}
    </CTX.Provider>
  );
}

export default Store;

サーバー

src/server/index.js
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http);

app.get('/', (req, res) => {
  res.send('<h1>Hello</h1>');
});

io.on('connection', socket => {
  console.log('a user connected');
  socket.on('chat message', msg => {
    console.log('message: ' + JSON.stringify(msg));
    io.emit('chat message', msg);
  });
  socket.on('add topic', topic => {
    console.log('topic: ' + topic);
    io.emit('add topic', topic);
  })
});

http.listen(3001, () => {
  console.log('listening on *:3001');
});

起動

$ node index.js

まとめ

material-ui使うときれいな見た目になる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Storybook + React.jsでViewにロジックや静的ファイルを導入する

背景

  • Storybook とはコンポーネント毎にViewを可視化するツールです。
  • i18nなどでテキストを管理していたため動的にファイルを変更させたかったのですが、公式ドキュメントなどを記述がありませんでしたので補足です。

下記のように記述すると、エラーで怒られます。

こんな風に使いたかった
import * as React from 'react'
import { useTranslation } from 'react-i18next'

// react-i18next を呼ぶ時は別途スクリプトの記述が必要ですが略
// 公式を参照するとわかります。
// https://github.com/i18next/react-i18next

storiesOf("HelloWorld", module)
  .add("HelloWorld", () => {
    const { t } = useTranslation()
    return (
      <HelloWorld>t('hello_world')</HelloWorld>
    )
  })
エラーメッセージ
Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

解決策

React.createElementを導入します。

これだと動きます
import * as React from 'react'
import { useTranslation } from 'react-i18next'

storiesOf("HelloWorld", module)
  .add("HelloWorld", () => 
    React.createElement(() => {
      const { t } = useTranslation()
      return (
        <HelloWorld>t('hello_world')</HelloWorld>
      )
    })
  )

静的ファイルの参照方法

webpackの出力先をstorybookにも適用してあげるだけです。-sオプションを使用します。

下記は webpackの出力先がdistの場合

package.json
{
// 
"scripts": {
    "storybook": "start-storybook -p 9001 -c .storybook -s ./dist",
  },
// 
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む