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

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

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

[Live Chat App with React Tutorial | React Hooks, Material UI, Socket.io, Node](https://www.youtube.com/watch?v=hiiaHyhhwBU)
を参考にしています（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使うときれいな見た目になる。