├── src
├── react-app-env.d.ts
├── setupTests.ts
├── App.test.tsx
├── componens
│ ├── Sidebar.tsx
│ ├── Avatar.tsx
│ ├── TodoList.tsx
│ ├── AutoBatchEventHandler.tsx
│ ├── TaskList.tsx
│ ├── AlbumList.tsx
│ ├── AutoBatchOther.tsx
│ ├── ReactQuery.tsx
│ └── Transition.tsx
├── index.css
├── reportWebVitals.ts
├── App.css
├── App.tsx
├── index.tsx
└── logo.svg
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── .gitignore
├── tsconfig.json
├── README.md
└── package.json
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reachscript-jak/react18-explanation-react18/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reachscript-jak/react18-explanation-react18/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reachscript-jak/react18-explanation-react18/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/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';
6 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/componens/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | export const Sidebar = () => {
2 | return (
3 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/.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/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/componens/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react"
2 |
3 | type Props = {
4 | children: ReactNode;
5 | isSelected?: boolean;
6 | onClick: (assignee: string) => void;
7 | }
8 |
9 | export const Avatar = ({ children, isSelected = false, onClick }: Props) => {
10 | const border = isSelected ? '3px solid orange': '1px solid gray';
11 | return (
12 | onClick(`${children}`)}>{children}
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/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 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/componens/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import axios from "axios";
3 |
4 | type Todo = {
5 | userId: number;
6 | id: number;
7 | title: string;
8 | completed: boolean;
9 | }
10 |
11 | const fetchTodos = async () => {
12 | const result = await axios.get('https://jsonplaceholder.typicode.com/todos');
13 | return result.data;
14 | }
15 |
16 | export const TodoList = () => {
17 | const { data } = useQuery(['todos'], fetchTodos);
18 |
19 | return (
20 |
21 |
TODO
22 | {data?.map((todo) =>
{todo.title}
)}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/componens/AutoBatchEventHandler.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { flushSync } from "react-dom";
3 |
4 | export const AutoBatchEventHandler = () => {
5 | console.log('AutoBatchEventHandler!!');
6 |
7 | const [state1, setState1] = useState(0);
8 | const [state2, setState2] = useState(0);
9 |
10 | const onClickUpdateButton = () => {
11 | flushSync(() => {
12 | setState1((state1) => state1 + 1)
13 | });
14 | setState2((state2) => state2 + 1);
15 | }
16 |
17 | return (
18 |
19 |
Automatic Batching確認用(イベントハンドラ)
20 |
21 |
State1: {state1}
22 |
State2: {state2}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/componens/TaskList.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useDeferredValue } from "react";
2 | import type { Task } from "./Transition"
3 |
4 | type Props = {
5 | taskList: Task[];
6 | }
7 |
8 | export const TaskList = memo(({ taskList }: Props) => {
9 | const deferredTaskList = useDeferredValue(taskList);
10 |
11 | return (
12 | <>
13 | {deferredTaskList.map((task) => (
14 |
23 |
タイトル:{task.title}
24 |
担当:{task.assignee}
25 |
26 | ))}
27 | >
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { ErrorBoundary } from 'react-error-boundary';
3 | import './App.css';
4 | import { AutoBatchEventHandler } from './componens/AutoBatchEventHandler';
5 | import { AutoBatchOther } from './componens/AutoBatchOther';
6 | import { ReactQuery } from './componens/ReactQuery';
7 | import { Transition } from './componens/Transition';
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
全体エラーだよ〜}>
18 | 全体ローディング中だよ〜}>
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/src/componens/AlbumList.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import axios from "axios";
3 |
4 | const sleep = (ms: number): Promise => {
5 | return new Promise((resolve) => {
6 | setTimeout(resolve, ms);
7 | });
8 | }
9 |
10 | type Album = {
11 | userId: number;
12 | id: number;
13 | title: string;
14 | }
15 |
16 | const fetchAlbums = async () => {
17 | const result = await axios.get('https://jsonplaceholder.typicode.com/albums').then(await sleep(5000));
18 | return result.data;
19 | }
20 |
21 | export const AlbumList = () => {
22 | const { data } = useQuery(['albums'], fetchAlbums);
23 |
24 | return (
25 |
26 |
アルバム
27 | {data?.map((album) =>
{album.title}
)}
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7 |
8 | const queryClient = new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | suspense: true,
12 | },
13 | },
14 | });
15 |
16 | const root = ReactDOM.createRoot(
17 | document.getElementById('root') as HTMLElement
18 | );
19 | root.render(
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 | // If you want to start measuring performance in your app, pass a function
28 | // to log results (for example: reportWebVitals(console.log))
29 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
30 | reportWebVitals();
31 |
--------------------------------------------------------------------------------
/src/componens/AutoBatchOther.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { flushSync } from "react-dom";
3 |
4 | type Todo = {
5 | userId: number;
6 | id: number;
7 | title: string;
8 | completed: boolean;
9 | }
10 |
11 | export const AutoBatchOther = () => {
12 | console.log('AutoBatchOther!!');
13 |
14 | const [todos, setTodos] = useState(null);
15 | const [isFinishApi, setIsFinishApi] = useState(false);
16 |
17 | // Promise内等イベントハンドラ以外の場所ではAitomatic Batchingされていなかった
18 | const onClickExecuteApi = () => {
19 | fetch('https://jsonplaceholder.typicode.com/todos')
20 | .then((res) => res.json())
21 | .then((data) => {
22 | flushSync(() => {
23 | setTodos(data);
24 | });
25 | setIsFinishApi(true);
26 | })
27 | }
28 |
29 | return (
30 |
31 |
Automatic Batching確認用(その他)
32 |
33 |
isFinishApi: {isFinishApi ? 'true' : 'false'}
34 | {todos?.map((todo) =>
{todo.title}
)}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react18-explanation-react18
2 |
3 | ## コードと実装差分
4 |
5 | コースのレクチャー毎に実装している内容を追いやすいように、その段階でのコードと実装差分を見れるようにしています。
6 |
7 | 本リポジトリはコースを作成する前に実装したものなので多少の違いはあるかもしれませんが、ほとんど同様の内容となっています。
8 |
9 | - 3.Automatic Batching
10 | - [コード](https://github.com/reachscript-jak/react18-explanation-react18/tree/bda31e49a97a2c4f8dd9a4e387a329053aaa0406)
11 | - [差分](https://github.com/reachscript-jak/react18-explanation-react18/commit/bda31e49a97a2c4f8dd9a4e387a329053aaa0406)
12 | - [プルリク](https://github.com/reachscript-jak/react18-explanation-react18/pull/1)
13 |
14 | - 4.Transition
15 | - [コード](https://github.com/reachscript-jak/react18-explanation-react18/tree/4cd537cb78722b6cdc55c4f8861b2c45b75d3aa2)
16 | - [差分](https://github.com/reachscript-jak/react18-explanation-react18/commit/4cd537cb78722b6cdc55c4f8861b2c45b75d3aa2)
17 | - [プルリク](https://github.com/reachscript-jak/react18-explanation-react18/pull/2)
18 |
19 | - 5.Suspense - その1(文法編)
20 | - [コード](https://github.com/reachscript-jak/react18-explanation-react18/tree/db2905ce7514d11feb4e1e1431be27378096e4d8)
21 | - [差分](https://github.com/reachscript-jak/react18-explanation-react18/commit/db2905ce7514d11feb4e1e1431be27378096e4d8)
22 | - [プルリク](https://github.com/reachscript-jak/react18-explanation-react18/pull/3)
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react18-explanation-react18",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@tanstack/react-query": "^4.2.1",
7 | "@testing-library/jest-dom": "^5.14.1",
8 | "@testing-library/react": "^13.0.0",
9 | "@testing-library/user-event": "^13.2.1",
10 | "@types/jest": "^27.0.1",
11 | "@types/node": "^16.7.13",
12 | "@types/react": "^18.0.0",
13 | "@types/react-dom": "^18.0.0",
14 | "axios": "^0.27.2",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-error-boundary": "^3.1.4",
18 | "react-scripts": "5.0.1",
19 | "typescript": "^4.4.2",
20 | "web-vitals": "^2.1.0"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/componens/ReactQuery.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, useState, useTransition } from "react"
2 | import { ErrorBoundary } from "react-error-boundary"
3 | import { AlbumList } from "./AlbumList"
4 | import { Sidebar } from "./Sidebar"
5 | import { TodoList } from "./TodoList"
6 |
7 | type Tabs = 'todo' | 'album';
8 |
9 | export const ReactQuery = () => {
10 | const [selectedTab, setSelectedTab] = useState('todo');
11 | const [isPending, startTransition] = useTransition();
12 |
13 | const buttonStyle = {
14 | padding: '12px',
15 | fontSize: '16px',
16 | border: 'none',
17 | opacity: isPending ? 0.5 : 1,
18 | }
19 | const albumButtonStyle = {
20 | ...buttonStyle,
21 | backgroundColor: selectedTab === 'album' ? 'royalblue' : 'white',
22 | color: selectedTab === 'album' ? 'white' : 'black',
23 | }
24 | const todoButtonStyle = {
25 | ...buttonStyle,
26 | backgroundColor: selectedTab === 'todo' ? 'royalblue' : 'white',
27 | color: selectedTab === 'todo' ? 'white' : 'black',
28 | }
29 |
30 | const onClickTabButton = (tab: Tabs) => {
31 | startTransition(() => {
32 | setSelectedTab(tab);
33 | });
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
Todo or AlbumListエラーだよ〜}>
41 | Todo or AlbumListローディング中だよ〜}>
42 |
43 |
44 | {selectedTab === 'todo' ? : }
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/componens/Transition.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Avatar } from "./Avatar";
3 | import { ReactQuery } from "./ReactQuery";
4 | import { TaskList } from "./TaskList";
5 |
6 | export type Task = {
7 | id: number;
8 | title: string;
9 | assignee: string;
10 | }
11 |
12 | const member = {
13 | a: 'A',
14 | b: 'B',
15 | c: 'C',
16 | }
17 |
18 | const generateDummyTasks = (): Task[] => {
19 | return Array(10000).fill('').map((_, index) => {
20 | const addedIndex = index + 1;
21 | return {
22 | id: addedIndex,
23 | title: `タスク${addedIndex}`,
24 | assignee: addedIndex % 3 === 0 ? member.a : addedIndex % 2 === 0 ? member.b : member.c,
25 | }
26 | })
27 | }
28 | const tasks = generateDummyTasks();
29 |
30 | const filteringAssignee = (assignee: string) => {
31 | if (assignee === '') return tasks;
32 | return tasks.filter((task) => task.assignee === assignee);
33 | }
34 |
35 | export const Transition = () => {
36 | const [selectedAssignee, setSelectedAssignee] = useState('');
37 | const [taskList, setTaskList] = useState(tasks);
38 | const [isShowList, setIsShowList] = useState(false);
39 |
40 | const onClickAssignee = (assignee: string) => {
41 | setSelectedAssignee(assignee);
42 | setTaskList(filteringAssignee(assignee));
43 | }
44 |
45 | return (
46 |
47 |
transition
48 |
49 |
{member.a}
50 |
{member.b}
51 |
{member.c}
52 |
53 |
54 |
55 |
56 |
57 |
58 | {isShowList &&
}
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------