├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── App.test.js
├── AppContainer.js
├── Form.js
├── FormModal.js
├── Header.js
├── Hint.js
├── List.js
├── components
│ ├── button.js
│ ├── modal.js
│ └── tab-body-container.js
├── const
│ └── languages.js
├── contexts
│ └── ThemeContext.js
├── hoc
│ └── with-loading.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
└── setupTests.js
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React入門 未経験から1週間でReactをマスターする
2 |
3 | このレポジトリは、 [やっすんのYouTube大学](https://www.youtube.com/channel/UCajrdoGzHzDogrNrLYYmGsg/) で投稿している
4 |
5 | 「React入門 未経験から1週間でReactをマスターする」のコードをアップしているものです。
6 |
7 | # 入門の始め方
8 |
9 | この入門は、 動画(YouTube), 文章(Qiita), 動くコード(GitHub) の3つを提供している入門で、究極のReact入門になっています。
10 |
11 | そのため、YouTubeかQiita記事を見ながら、このレポジトリを利用してください。
12 |
13 |
14 | # 入門動画と対応するQiita記事一覧
15 |
16 | - その1. Reactの新規プロジェクトの作成と立ち上げ
17 | - [
](https://youtu.be/lEEC_NuIGQc)
18 | - [Qiita](https://qiita.com/yassun-youtube/items/2ae26050efd2133c2286)
19 | - その2. コンポーネントのプロパティ(props)とステート(state)
20 | - [
](https://youtu.be/8KV1CBcB2Yg)
21 | - [Qiita](https://qiita.com/yassun-youtube/items/ca91e2f9905fb8ca62d0)
22 | - その3. Class Components と Function Components
23 | - [
](https://youtu.be/ve85ejcYiZ0)
24 | - [Qiita](https://qiita.com/yassun-youtube/items/2ed8601e4fa477726705)
25 | - その4. 条件分岐 (if) と繰り返し (loop)
26 | - [
](https://youtu.be/ZLEMN2pCE8E)
27 | - [Qiita](https://qiita.com/yassun-youtube/items/9474b1681958e2ab0f25)
28 | - その5. フォームと親子間のデータのやり取り
29 | - [
](https://youtu.be/kEBP9WhifX0)
30 | - [Qiita](https://qiita.com/yassun-youtube/items/545e7d9c98fe919dc394)
31 | - その6. コンポーネントのライフサイクル
32 | - [
](https://youtu.be/SKrnW7PRBdk)
33 | - [Qiita](https://qiita.com/yassun-youtube/items/372464404ae6855e6d78)
34 | - その7. スタイル
35 | - [
](https://youtu.be/VxwI2tavv_M)
36 | - その8. Higher-Order Component (準備中)
37 | - [
](https://youtu.be/9iulWDK95TY)
38 | - その9. Portalを利用したモーダル (準備中)
39 | - その10. refによるエレメントの取得 (準備中)
40 | - その11. Contextを利用したテーマの変更 (準備中)
41 |
42 | # このアプリの起動方法
43 |
44 | ```
45 | yarn install
46 | ```
47 |
48 | で各種パッケージをインストールしたあとに、
49 |
50 | ```
51 | yarn start
52 | ```
53 |
54 | で `http://localhost:3000` にアプリが立ち上がります。
55 |
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tutorial",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.0",
12 | "styled-components": "^5.2.1",
13 | "web-vitals": "^0.2.4"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yassun-youtube/ReactTutorial/c522ab1776913c7a9805818243199a1956fd3bcb/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yassun-youtube/ReactTutorial/c522ab1776913c7a9805818243199a1956fd3bcb/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yassun-youtube/ReactTutorial/c522ab1776913c7a9805818243199a1956fd3bcb/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from 'react';
2 | import { List } from "./List";
3 | import { Form } from "./Form";
4 | import styled from 'styled-components';
5 | import { getLanguages } from "./const/languages";
6 | import { withLoading } from "./hoc/with-loading";
7 | import { Header } from "./Header";
8 | import { ThemeContext } from "./contexts/ThemeContext";
9 |
10 | const Container = styled.div`
11 | height: 100%;
12 | color: ${({ theme }) => theme.color};
13 | background-color: ${({ theme }) => theme.backgroundColor};
14 | `
15 |
16 | function App({ data }) {
17 | const [tab, setTab] = useState('list');
18 | const [langs, setLangs] = useState(data);
19 |
20 | const [theme] = useContext(ThemeContext);
21 |
22 | const addLang = (lang) => {
23 | setLangs([...langs, lang]);
24 | setTab('list');
25 | };
26 |
27 | return (
28 |
29 |
30 | {
31 | tab === 'list' ?
:
34 | );
35 | }
36 |
37 | export default App;
38 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/AppContainer.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { ThemeContext,THEMES } from "./contexts/ThemeContext";
3 | import { withLoading } from "./hoc/with-loading";
4 | import App from "./App";
5 | import { getLanguages } from "./const/languages";
6 |
7 | const AppComponent = withLoading(App, getLanguages)
8 |
9 | export const AppContainer = () => {
10 | const [theme, setTheme] = useState(THEMES.dark);
11 |
12 | const toggleTheme = () => {
13 | const nextTheme = (theme === THEMES.dark) ? THEMES.light : THEMES.dark;
14 | setTheme(nextTheme);
15 | }
16 |
17 | return (
18 |
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/src/Form.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Button } from "./components/button";
4 | import { TabBodyContainer } from "./components/tab-body-container";
5 | import { FormModal } from "./FormModal";
6 | import { Hint } from "./Hint";
7 |
8 | const Label = styled.label`
9 | display: flex;
10 | color: #757575;
11 | font-size: 14px;
12 | font-weight: bold;
13 | `
14 | const Input = styled.input`
15 | border-radius: 3px;
16 | padding: 4px 8px;
17 | border: 1px solid black;
18 | `
19 | const ButtonContainer = styled.div`
20 | margin-top: 24px;
21 | `
22 | const FormButton = styled(Button)`
23 | width: 120px;
24 | `
25 |
26 | export const Form = ({ onAddLang }) => {
27 | const [text, setText] = useState('');
28 | const [showModal, setShowModal] = useState(false);
29 |
30 | const submitForm = (e) => {
31 | e.preventDefault();
32 | setShowModal(true);
33 | }
34 |
35 | return (
36 |
37 |
47 | {
48 | showModal &&
49 | onAddLang(text)}
51 | cancel={() => setShowModal(false)}
52 | />
53 | }
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/FormModal.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import styled from 'styled-components';
3 | import { Modal } from "./components/modal";
4 | import { Button } from "./components/button";
5 | import { ThemeContext,THEMES } from "./contexts/ThemeContext";
6 |
7 | const Container = styled.div`
8 | width: 240px;
9 | border-radius: 10px;
10 | padding: 24px 36px;
11 | color: ${({ theme }) => theme.color};
12 | background-color: ${({ theme }) => theme.backgroundColor};
13 | border: ${({ theme }) => theme === THEMES.dark ? '2px solid white' : 'none'};
14 | `
15 |
16 | const ButtonWrapper = styled.div`
17 | display: flex;
18 | justify-content: space-around;
19 | margin-top: 24px;
20 | `
21 |
22 | export const FormModal = ({ confirm, cancel }) => {
23 | const [theme] = useContext(ThemeContext);
24 | return (
25 |
26 |
27 | 本当に作成しますか?
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/Header.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import styled from "styled-components";
3 | import { Button } from "./components/button";
4 | import { ThemeContext } from "./contexts/ThemeContext";
5 |
6 | const Container = styled.header`
7 | display: flex;
8 | justify-content: space-between;
9 | padding: 24px 64px 0;
10 | border-bottom: 1px solid #E0E0E0;
11 | `
12 |
13 | const HeaderUl = styled.ul`
14 | display: flex;
15 | margin: 0;
16 | padding: 0;
17 | `
18 | const HeaderLi = styled.li`
19 | list-style: none;
20 | padding: 4px 12px;
21 | cursor: pointer;
22 | border-bottom: ${props => props.focused ? '2px solid #F44336' : 'none' };
23 | `
24 |
25 | const HeaderButton = styled(Button)`
26 | padding: 0;
27 | margin-bottom: 4px;
28 | `
29 |
30 | export const Header = ({ tab, setTab }) => {
31 | const [theme, toggleTheme] = useContext(ThemeContext);
32 | return (
33 |
34 |
35 | setTab('list')}>リスト
36 | setTab('form')}>フォーム
37 |
38 | テーマ変更
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/Hint.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useState, useRef, useEffect } from 'react';
3 |
4 | const HintContainer = styled.div`
5 | position: relative;
6 | display: inline-flex;
7 | margin-left: 12px;
8 | `
9 |
10 | const HintInner = styled.div`
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | border-radius: 50%;
15 | border: 1px solid #757575;
16 | width: 24px;
17 | height: 24px;
18 | cursor: pointer;
19 | `;
20 |
21 | const PopupContainer = styled.div`
22 | position: absolute;
23 | left: 88%;
24 | bottom: 12px;
25 | display: flex;
26 | justify-content: center;
27 | padding: 8px;
28 | width: 140px;
29 | border: 1px solid black;
30 | border-radius: 8px;
31 | `
32 |
33 | export const Hint = () => {
34 | const [showPopup, setShowPopup] = useState(false);
35 |
36 | const ref = useRef(null);
37 |
38 | useEffect(() => {
39 | if (ref.current) ref.current.focus();
40 | })
41 | return (
42 |
43 | setShowPopup(true)}>
44 | ?
45 |
46 | {
47 | showPopup && (
48 | setShowPopup(false)} tabIndex={0}>
49 | 言語の名前です
50 |
51 | )
52 | }
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/List.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { TabBodyContainer } from "./components/tab-body-container";
3 |
4 | const ListItem = styled.div`
5 | padding: 8px 16px;
6 |
7 | &:nth-child(n+2) {
8 | border-top: 1px solid #D9DBDE;
9 | }
10 | `
11 |
12 | export const List = ({ langs }) => {
13 | return (
14 |
15 |
16 | {
17 | langs.map((lang, index) => {
18 | return { lang }
19 | })
20 | }
21 |
22 |
23 | )
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/button.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Button = styled.button`
4 | border: none;
5 | border-radius: 3px;
6 | background-color: #2196F3;
7 | padding: 8px 16px;
8 | min-width: 100px;
9 | font-size: 14px;
10 | font-weight: bold;
11 | color: white;
12 | cursor: pointer;
13 | `
14 |
--------------------------------------------------------------------------------
/src/components/modal.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import { useEffect, useContext } from 'react';
3 | import styled from 'styled-components';
4 |
5 | const modalRoot = document.getElementById('modal-root');
6 |
7 | const Container = styled.div`
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | height: 100%;
15 | width: 100%;
16 | background-color: rgba(0, 0, 0, .5);
17 | `
18 |
19 | export const Modal = (props) => {
20 | return ReactDOM.createPortal(
21 |
22 | { props.children }
23 | ,
24 | modalRoot
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/tab-body-container.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Container = styled.div`
4 | padding: 12px 64px;
5 | `
6 |
7 | export const TabBodyContainer = ({ children, title }) => {
8 | return (
9 |
10 | { title }
11 | { children }
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/const/languages.js:
--------------------------------------------------------------------------------
1 | const LANGUAGES = [
2 | 'JavaScript',
3 | 'C++',
4 | 'Ruby',
5 | 'Java',
6 | 'PHP',
7 | 'Go'
8 | ];
9 |
10 | export const getLanguages = () => {
11 | return new Promise((resolve) => {
12 | setTimeout(() => {
13 | resolve(LANGUAGES);
14 | }, 1000);
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/src/contexts/ThemeContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const THEMES = {
4 | light: {
5 | color: 'black',
6 | backgroundColor: 'white',
7 | },
8 | dark: {
9 | color: 'white',
10 | backgroundColor: '#222222',
11 | }
12 | }
13 |
14 | export const ThemeContext = React.createContext([THEMES.dark, () => {}]);
15 |
16 |
--------------------------------------------------------------------------------
/src/hoc/with-loading.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { useState, useEffect, useContext } from 'react';
3 | import { ThemeContext } from "../contexts/ThemeContext";
4 |
5 | const LoadDiv = styled.div`
6 | height: 100%;
7 | padding: 36px;
8 | color: ${({ theme }) => theme.color};
9 | background-color: ${({ theme }) => theme.backgroundColor};
10 | `
11 |
12 | export const withLoading = (WrappedComponent, fetchData) => {
13 | return () => {
14 | const [data, setData] = useState(null);
15 | const [theme] = useContext(ThemeContext);
16 |
17 | useEffect(() => {
18 | fetch();
19 | }, [])
20 |
21 | const fetch = async () => {
22 | const data = await fetchData();
23 | setData(data);
24 | }
25 |
26 | const Loading = (
27 | ロード中...
28 | )
29 |
30 | return data ? : Loading;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | height: 100%;
3 | }
4 | body {
5 | height: 100%;
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | }
13 |
14 | code {
15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
16 | monospace;
17 | }
18 | #root {
19 | height: 100%;
20 | }
21 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import reportWebVitals from './reportWebVitals';
5 | import { AppContainer } from "./AppContainer";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
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';
6 |
--------------------------------------------------------------------------------