├── src
├── react-app-env.d.ts
├── states
│ ├── Tasks.ts
│ ├── User.ts
│ └── index.ts
├── sagas
│ ├── PromiseGenericType.ts
│ ├── index.ts
│ └── User
│ │ ├── index.ts
│ │ └── GetUserSaga.ts
├── apis
│ ├── User
│ │ ├── Model.ts
│ │ └── GetUserApi.ts
│ └── Axios.ts
├── actions
│ ├── Tasks
│ │ ├── ActionType.ts
│ │ ├── Action.ts
│ │ └── ActionCreator.ts
│ └── User
│ │ ├── ActionType.ts
│ │ ├── Action.ts
│ │ └── ActionCreator.ts
├── styles
│ ├── Color.ts
│ ├── Font.ts
│ └── GridArea.ts
├── components
│ ├── App.tsx
│ ├── Organisms
│ │ ├── AddTaskArea.tsx
│ │ └── ProfileArea.tsx
│ ├── Pages
│ │ └── TaskPage.tsx
│ ├── Atoms
│ │ ├── Form.tsx
│ │ ├── Button.tsx
│ │ ├── Label.tsx
│ │ └── ListLabel.tsx
│ ├── Templates
│ │ └── TaskTemplate.tsx
│ └── Molecules
│ │ └── AddTask.tsx
├── reducers
│ ├── index.ts
│ ├── Tasks.ts
│ └── User.ts
├── setupTests.ts
├── index.css
├── index.tsx
└── serviceWorker.ts
├── mock_server
├── jsons
│ ├── tasks.json
│ └── user.json
└── server.js
├── public
├── robots.txt
├── manifest.json
└── index.html
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/mock_server/jsons/tasks.json:
--------------------------------------------------------------------------------
1 | [
2 | "洗濯する",
3 | "買い物する",
4 | "部屋掃除する"
5 | ]
--------------------------------------------------------------------------------
/mock_server/jsons/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Shotaro Okada",
3 | "age": 22
4 | }
--------------------------------------------------------------------------------
/src/states/Tasks.ts:
--------------------------------------------------------------------------------
1 | type TasksState = string[]
2 |
3 | export default TasksState
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/sagas/PromiseGenericType.ts:
--------------------------------------------------------------------------------
1 | export type PromiseGenericType = T extends Promise ? U : T;
--------------------------------------------------------------------------------
/src/states/User.ts:
--------------------------------------------------------------------------------
1 | type UserState = {
2 | name: string,
3 | age: number
4 | }
5 |
6 | export default UserState
--------------------------------------------------------------------------------
/src/apis/User/Model.ts:
--------------------------------------------------------------------------------
1 | type UserModel = {
2 | name: string,
3 | age: number
4 | }
5 |
6 | export default UserModel;
--------------------------------------------------------------------------------
/src/actions/Tasks/ActionType.ts:
--------------------------------------------------------------------------------
1 | enum TasksActionType {
2 | ADD_TASK = 'ADD_TASK'
3 | }
4 |
5 | export default TasksActionType;
--------------------------------------------------------------------------------
/src/apis/Axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default axios.create({
4 | baseURL: 'http://127.0.0.1:3001/'
5 | })
--------------------------------------------------------------------------------
/src/styles/Color.ts:
--------------------------------------------------------------------------------
1 | enum Color {
2 | Gray = '#424242',
3 | RoyalBlue = 'royalblue',
4 | LightGray = '#70757a',
5 | WhiteSmoke = 'whitesmoke'
6 | }
7 |
8 | export default Color
--------------------------------------------------------------------------------
/src/states/index.ts:
--------------------------------------------------------------------------------
1 | import { StateType } from "typesafe-actions";
2 | import rootReducer from "../reducers";
3 |
4 | type RootState = StateType
5 |
6 | export default RootState;
--------------------------------------------------------------------------------
/src/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { all } from "redux-saga/effects";
2 | import { userSaga } from "./User";
3 |
4 | export default function* rootSaga() {
5 | yield all([
6 | ...userSaga,
7 | ])
8 | }
--------------------------------------------------------------------------------
/src/styles/Font.ts:
--------------------------------------------------------------------------------
1 | export enum FontFamily {
2 | Mairyo = 'Mairyo',
3 | Roboto = 'Roboto',
4 | }
5 |
6 | export enum FontSize {
7 | Small = 13,
8 | Medium = 16,
9 | Large = 20
10 | }
11 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
--------------------------------------------------------------------------------
/src/actions/Tasks/Action.ts:
--------------------------------------------------------------------------------
1 | import * as ActionCreators from './ActionCreator';
2 | import { ActionType } from 'typesafe-actions';
3 |
4 | type TasksAction = ActionType;
5 |
6 | export default TasksAction;
--------------------------------------------------------------------------------
/src/actions/Tasks/ActionCreator.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from "typesafe-actions";
2 | import TasksActionType from "./ActionType";
3 |
4 | export const addTask = createAction(
5 | TasksActionType.ADD_TASK
6 | )();
--------------------------------------------------------------------------------
/src/actions/User/ActionType.ts:
--------------------------------------------------------------------------------
1 | enum UserActionType {
2 | GET_USER_REQUEST = 'GET_USER_REQUEST',
3 | GET_USER_SUCCESS = 'GET_USER_SUCCESS',
4 | GET_USER_FAIL = 'GET_USER_FAIL'
5 | }
6 |
7 | export default UserActionType;
--------------------------------------------------------------------------------
/src/actions/User/Action.ts:
--------------------------------------------------------------------------------
1 | import * as ActionCreators from './ActionCreator';
2 | import { ActionType } from 'typesafe-actions';
3 |
4 | type UserAction = ActionType
5 |
6 | export default UserAction;
7 |
--------------------------------------------------------------------------------
/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TaskPage from './Pages/TaskPage';
3 |
4 | // ログインの画面処理や画面遷移などを記述
5 | function App() {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import user from './User';
3 | import tasks from './Tasks'
4 |
5 | const rootReducer = combineReducers({
6 | user,
7 | tasks,
8 | });
9 |
10 | export default rootReducer;
--------------------------------------------------------------------------------
/src/styles/GridArea.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | type Props = {
4 | area: string
5 | }
6 |
7 | const GridArea = styled.div(props => `
8 | grid-area: ${props.area};
9 | position: relative;
10 | `);
11 |
12 | export default GridArea;
--------------------------------------------------------------------------------
/src/sagas/User/index.ts:
--------------------------------------------------------------------------------
1 | import { takeLatest } from "redux-saga/effects";
2 | import UserActionType from "../../actions/User/ActionType";
3 | import { getUserSaga } from "./GetUserSaga";
4 |
5 | export const userSaga = [
6 | takeLatest(UserActionType.GET_USER_REQUEST, getUserSaga),
7 | ];
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/apis/User/GetUserApi.ts:
--------------------------------------------------------------------------------
1 | import Axios from "../Axios";
2 | import UserModel from "./Model";
3 |
4 | export type GetUserParam = {
5 | id: string
6 | }
7 |
8 | export async function getUserApi({ id }: GetUserParam) {
9 | try {
10 | return await Axios.get('/user', {
11 | params: {
12 | id
13 | }
14 | })
15 | } catch (e) {
16 | throw new Error(e)
17 | }
18 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Organisms/AddTaskArea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AddTask from '../Molecules/AddTask'
3 | import GridArea from '../../styles/GridArea'
4 |
5 | type Props = {
6 | area: string;
7 | }
8 |
9 | const AddTaskArea: React.FC = (props) => {
10 | const { area } = props
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default AddTaskArea
--------------------------------------------------------------------------------
/src/actions/User/ActionCreator.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncAction } from "typesafe-actions"
2 | import UserActionType from "./ActionType"
3 | import UserState from "../../states/User";
4 | import { GetUserParam } from "../../apis/User/GetUserApi";
5 |
6 |
7 | export const getUsers = createAsyncAction(
8 | UserActionType.GET_USER_REQUEST,
9 | UserActionType.GET_USER_SUCCESS,
10 | UserActionType.GET_USER_FAIL
11 | )();
--------------------------------------------------------------------------------
/src/reducers/Tasks.ts:
--------------------------------------------------------------------------------
1 | import TasksState from "../states/Tasks";
2 | import TasksAction from "../actions/Tasks/Action";
3 | import TasksActionType from "../actions/Tasks/ActionType";
4 |
5 | const initialState: TasksState = [];
6 |
7 | export default (state: TasksState = initialState, action: TasksAction): TasksState => {
8 | switch (action.type) {
9 | case TasksActionType.ADD_TASK:
10 | return [
11 | ...state,
12 | action.payload
13 | ]
14 | default:
15 | return state
16 | }
17 | }
--------------------------------------------------------------------------------
/src/components/Pages/TaskPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import TaskTemplate from '../Templates/TaskTemplate';
3 | import { useDispatch } from 'react-redux';
4 | import { getUsers } from '../../actions/User/ActionCreator';
5 |
6 | // ここでこのページを描画するために必要なデータを取得する
7 | const TaskPage: React.FC = () => {
8 | const dispatch = useDispatch()
9 | useEffect(() => {
10 | dispatch(getUsers.request({ id: "user01" }));
11 | })
12 | return (
13 |
14 | )
15 | }
16 |
17 | export default TaskPage;
--------------------------------------------------------------------------------
/src/reducers/User.ts:
--------------------------------------------------------------------------------
1 | import UserState from "../states/User";
2 | import UserAction from "../actions/User/Action";
3 | import UserActionType from "../actions/User/ActionType";
4 |
5 | const initialState: UserState = {
6 | name: '',
7 | age: 0
8 | }
9 |
10 | export default (state: UserState = initialState, action: UserAction): UserState => {
11 | switch (action.type) {
12 | case UserActionType.GET_USER_SUCCESS:
13 | return {
14 | ...action.payload
15 | }
16 | default:
17 | return state
18 | }
19 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Atoms/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { FontSize, FontFamily } from '../../styles/Font';
4 |
5 | type Props = {
6 | value: string;
7 | onChange: (e: React.FormEvent) => void;
8 | }
9 |
10 | const Form: React.FC = (props) => {
11 | const { value, onChange } = props
12 | return (
13 |
14 | )
15 | }
16 |
17 | export default Form;
18 |
19 | const StyledForm = styled.input`
20 | font-size: ${FontSize.Medium}px;
21 | font-family: ${FontFamily.Roboto};
22 | `
--------------------------------------------------------------------------------
/src/components/Atoms/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { FontSize, FontFamily } from '../../styles/Font';
4 |
5 | type Props = {
6 | label: string;
7 | onClick: (e: React.MouseEvent) => void;
8 | }
9 |
10 | const Button: React.FC = (props) => {
11 | const { label, onClick } = props;
12 | return (
13 |
14 | {label}
15 |
16 | )
17 | }
18 |
19 | export default Button;
20 |
21 | const StyledButton = styled.button`
22 | font-size: ${FontSize.Medium}px;
23 | font-family: ${FontFamily.Roboto};
24 | margin: 8px;
25 | `
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | React App
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Atoms/Label.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontSize, FontFamily } from '../../styles/Font';
3 | import styled from 'styled-components';
4 |
5 | type Props = {
6 | fontSize?: FontSize,
7 | text: string | number
8 | }
9 |
10 | const Label: React.FC = (props) => {
11 | const { fontSize = FontSize.Medium, text } = props;
12 | return (
13 |
14 | {text}
15 |
16 | )
17 | }
18 |
19 | export default Label;
20 |
21 | type StyledLabelProps = {
22 | fontSize: FontSize
23 | }
24 |
25 | const StyledLabel = styled.div(props => `
26 | font-size: ${props.fontSize};
27 | font-family: ${FontFamily.Roboto};
28 | margin: 8px;
29 | `)
--------------------------------------------------------------------------------
/src/components/Templates/TaskTemplate.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProfileArea from '../Organisms/ProfileArea';
3 | import AddTaskArea from '../Organisms/AddTaskArea';
4 | import styled from 'styled-components';
5 |
6 | enum Area {
7 | Profile = 'Profile',
8 | AddTask = 'AddTask',
9 | }
10 |
11 | // Gridの設定をし、areaを指定してOrganismsを呼び出す
12 | const TaskTemplate: React.FC = () => {
13 | return (
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default TaskTemplate;
22 |
23 | const GridLayout = styled.div`
24 | display: grid;
25 | grid-template-columns: 1fr 1fr;
26 | grid-template-areas:
27 | " Profile AddTask "
28 | `;
--------------------------------------------------------------------------------
/src/components/Atoms/ListLabel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontSize, FontFamily } from '../../styles/Font';
3 | import styled from 'styled-components';
4 |
5 | type Props = {
6 | fontSize?: FontSize,
7 | text: string | number
8 | }
9 |
10 | const ListLabel: React.FC = (props) => {
11 | const { fontSize = FontSize.Medium, text } = props;
12 | return (
13 |
14 | {text}
15 |
16 | )
17 | }
18 |
19 | export default ListLabel;
20 |
21 | type StyledListLabelProps = {
22 | fontSize: FontSize
23 | }
24 |
25 | const StyledLabel = styled.li(props => `
26 | font-size: ${props.fontSize}px;
27 | font-family: ${FontFamily.Roboto};
28 | margin-left: 16px;
29 | `)
--------------------------------------------------------------------------------
/src/sagas/User/GetUserSaga.ts:
--------------------------------------------------------------------------------
1 | import { getUsers } from "../../actions/User/ActionCreator";
2 | import { PromiseGenericType } from "../PromiseGenericType";
3 | import { getUserApi } from "../../apis/User/GetUserApi";
4 | import { call, put } from "redux-saga/effects";
5 |
6 | export function* getUserSaga(action: ReturnType) {
7 | try {
8 | const response: PromiseGenericType> = yield call(
9 | getUserApi,
10 | action.payload
11 | );
12 | if (response.status === 200 && response.data) {
13 | yield put(getUsers.success(response.data));
14 | } else {
15 | yield put(getUsers.failure(new Error('fail get user')))
16 | }
17 | } catch (e) {
18 | yield put(getUsers.failure(new Error('fail get user in api')))
19 | }
20 | }
--------------------------------------------------------------------------------
/src/components/Molecules/AddTask.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import Button from '../Atoms/Button';
4 | import Form from '../Atoms/Form';
5 | import { addTask } from '../../actions/Tasks/ActionCreator';
6 |
7 | const AddTask: React.FC = () => {
8 | const dispatch = useDispatch();
9 | const [inputTask, setInputTask] = useState('');
10 |
11 | const onChange = (e: React.FormEvent) => {
12 | setInputTask(e.currentTarget.value);
13 | }
14 |
15 | const onClick = (e: React.MouseEvent) => {
16 | e.preventDefault();
17 | if (inputTask === '') {
18 | return
19 | }
20 | dispatch(addTask(inputTask))
21 | setInputTask('');
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default AddTask;
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 | import * as serviceWorker from './serviceWorker';
5 | import rootSaga from './sagas';
6 | import { createStore, applyMiddleware } from 'redux';
7 | import rootReducer from './reducers';
8 | import createSagaMiddleware from 'redux-saga';
9 | import { composeWithDevTools } from 'redux-devtools-extension';
10 | import { createLogger } from 'redux-logger';
11 | import { Provider } from 'react-redux';
12 |
13 | const composeEnhancers = composeWithDevTools({});
14 | const logger = createLogger();
15 | const sagaMiddleware = createSagaMiddleware();
16 | const middleware = [logger, sagaMiddleware];
17 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(...middleware)));
18 | sagaMiddleware.run(rootSaga);
19 |
20 | ReactDOM.render(
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
27 | serviceWorker.unregister();
28 |
--------------------------------------------------------------------------------
/mock_server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const user = require('./jsons/user.json')
3 | const tasks = require('./jsons/tasks.json')
4 |
5 | const app = express();
6 | const port_number = 3001;
7 |
8 | // CORS対策
9 | app.use((_, res, next) => {
10 | res.header('Access-Control-Allow-Origin', '*');
11 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
12 | next();
13 | });
14 |
15 | app.get('/', (_, res) => res.send('React Redux Example Mock Server'));
16 |
17 | app.get('/user', (req, res) => {
18 | const id = req.query.id
19 | if (id == "user01") {
20 | res.status(200).json(user)
21 | } else {
22 | res.status(400).send('id is not exist')
23 | }
24 | });
25 |
26 | app.get('/tasks', (req, res) => {
27 | const id = req.query.id
28 | if (id == "user01") {
29 | res.status(200).json(tasks)
30 | } else {
31 | res.status(400).send('id is not exist')
32 | }
33 | })
34 |
35 | app.listen(port_number, () => console.log('Listening on Port' + port_number));
36 |
37 |
--------------------------------------------------------------------------------
/src/components/Organisms/ProfileArea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import RootState from '../../states';
4 | import Label from '../Atoms/Label';
5 | import ListLabel from '../Atoms/ListLabel';
6 | import GridArea from '../../styles/GridArea';
7 |
8 | type Props = {
9 | area: string
10 | }
11 | const ProfileArea: React.FC = (props) => {
12 | const { area } = props;
13 | const user = useSelector(state => state.user);
14 | const tasks = useSelector(state => state.tasks);
15 | if (user.name) {
16 | return (
17 |
18 |
19 |
20 |
21 | {tasks.map((task, index) => {
22 | return
23 | })}
24 |
25 | )
26 | } else {
27 | return (
28 |
29 |
30 |
31 | )
32 | }
33 | }
34 |
35 | export default ProfileArea;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_redux_example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/axios": "^0.14.0",
10 | "@types/jest": "^24.0.0",
11 | "@types/node": "^12.0.0",
12 | "@types/react": "^16.9.0",
13 | "@types/react-dom": "^16.9.0",
14 | "@types/react-redux": "^7.1.7",
15 | "@types/redux": "^3.6.0",
16 | "@types/redux-logger": "^3.0.7",
17 | "@types/redux-saga": "^0.10.5",
18 | "@types/styled-components": "^5.0.0",
19 | "axios": "^0.19.2",
20 | "express": "^4.17.1",
21 | "react": "^16.12.0",
22 | "react-dom": "^16.12.0",
23 | "react-redux": "^7.2.0",
24 | "react-scripts": "3.4.0",
25 | "redux": "^4.0.5",
26 | "redux-devtools-extension": "^2.13.8",
27 | "redux-logger": "^3.0.6",
28 | "redux-saga": "^1.1.3",
29 | "styled-components": "^5.0.1",
30 | "typesafe-actions": "^5.1.0",
31 | "typescript": "~3.7.2"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "mock-server": "node mock_server/server.js",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject"
39 | },
40 | "eslintConfig": {
41 | "extends": "react-app"
42 | },
43 | "browserslist": {
44 | "production": [
45 | ">0.2%",
46 | "not dead",
47 | "not op_mini all"
48 | ],
49 | "development": [
50 | "last 1 chrome version",
51 | "last 1 firefox version",
52 | "last 1 safari version"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 実行環境
2 | - node 13.6.0
3 | - yarn 1.21.1
4 |
5 | ## 環境構築方法
6 | 1. [nodeをインストール](https://nodejs.org/ja/)
7 | 2. yarnをインストール(コマンドライン上にて`npm install -g yarn`)
8 | 3. (`Error: Cannot find module 'express'`と言われた場合は、コマンドライン上にて`yarn add express`)
9 |
10 | ## アプリ実行方法
11 | * 初回clone時には `yarn install` する
12 |
13 | (ターミナルを2つ使います)
14 | 1. `yarn mock-server` これでモックサーバを立ち上げる
15 | 2. `yarn start` React Appの実行
16 |
17 | ## 使用技術等
18 | - 言語:TypeScript
19 | - UIライブラリ:React, styled-components
20 | - State管理:Redux
21 | - フォルダ構成:Redux Way
22 | - ミドルウェア:Redux Saga
23 | - React Componentsの切り方:Atomic Design
24 | - HTTP通信:Axios
25 | - モックサーバ:JavaScript&Express
26 |
27 | ## 参考資料
28 | (実際に書いたり、チームメンバーに聞く方が100倍効率が良いので、1回くらい読み流す程度でok)
29 | - [Reactとは(useStateまで読めばok)](https://sbfl.net/blog/2019/11/12/react-hooks-introduction/)
30 | - [Reduxの概念であるFluxについて](https://medium.com/samyamashita/%E6%BC%AB%E7%94%BB%E3%81%A7%E8%AA%AC%E6%98%8E%E3%81%99%E3%82%8B-flux-1a219e50232b)
31 | - [Reduxとは(各役割が何をしたいかだけ抑えればok)](https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7)
32 | - [Atomic Designとは](https://www.slideshare.net/ygoto3q/organizing-design-with-atomic-design-104872303?from_m_app=ios)
33 |
34 | ## コードの読み方について(Redux)
35 | 読む順番はstates→actions→reducers→componentsがオススメ
36 |
37 | これが理解できるようになったら、actions→sagas→apis→sagas→actionsという流れも理解しよう!
38 | - states: アプリにて管理したい情報の型を定義(例:ユーザ名)
39 | - actions: アプリにて管理したい情報が変更される状況を記述(例:ユーザ名を編集)
40 | - reducers: 情報の具体的な変更処理を記述(例:ユーザ名を太郎から次郎に変更)
41 | - components: UI(アプリの見た目)を記述
42 | - sagas: 非同期処理を行う(例:ユーザ名の取得と見た目の表示を同時に行う)
43 | - api: 通信処理を記述(例:サーバからユーザ名を取得する)
44 |
45 | ## コードの読み方について(React)
46 | 以下、componentsフォルダ内の説明です。
47 | 下に行くほど大きいComponentになっています。
48 | - Atoms:UIとしての最小単位 → Button, Form
49 | - Molecules:UXとしての最小単位 → Login Form, Add Task
50 | - Organisms:1つの領域を表現 → Header, Side Bar
51 | - Templates:画面の領域分けを表現 → Headerは上部20%, Side Barは左30%… (今回はGridLayoutによって表現)
52 | - Pages:初期データの取得、Templatesに実データを渡すなど
53 | - App.tsx:ログイン等の画面処理、CSSの初期化、画面遷移など
54 |
55 | ## 演習問題
56 | - ボタンやフォームの色や大きさを変えてみる(React, styled-components)
57 | - 画面の領域分けを変えてみる(React, styled-components, Grid Layout)
58 | - タスクの削除機能を実装してみる(React, Redux)
59 | - 画面遷移を実装してみる(React, React Router)
60 | - モックサーバからtasksを取得してみる(Axios, Redux, Redux Saga)
61 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready
142 | .then(registration => {
143 | registration.unregister();
144 | })
145 | .catch(error => {
146 | console.error(error.message);
147 | });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------