diff --git a/client/jsconfig.json b/client/jsconfig.json
index b8d6842..10781e7 100644
--- a/client/jsconfig.json
+++ b/client/jsconfig.json
@@ -1,7 +1,12 @@
{
"compilerOptions": {
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@/hooks/*": ["./src/lib/hooks/*"],
+ "@/public/*": ["./public/*"],
+ "@/services/*": ["./src/lib/services/*"],
+ "@/store/*": ["./src/lib/store/*"],
+ "@/utils/*": ["./src/lib/utils/*"]
}
}
}
diff --git a/client/package-lock.json b/client/package-lock.json
index 98675d8..56b640f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,11 +8,13 @@
"name": "client",
"version": "0.1.0",
"dependencies": {
+ "@reduxjs/toolkit": "^1.9.3",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"next": "13.2.4",
"react": "18.2.0",
- "react-dom": "18.2.0"
+ "react-dom": "18.2.0",
+ "react-redux": "^8.0.5"
},
"engines": {
"node": "18.14.2",
@@ -371,6 +373,29 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
+ "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
+ "dependencies": {
+ "immer": "^9.0.16",
+ "redux": "^4.2.0",
+ "redux-thunk": "^2.4.2",
+ "reselect": "^4.1.7"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.0.2"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rushstack/eslint-patch": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
@@ -384,11 +409,45 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "node_modules/@types/react": {
+ "version": "18.0.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
+ "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@typescript-eslint/parser": {
"version": "5.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.55.0.tgz",
@@ -784,6 +843,11 @@
"node": ">= 8"
}
},
+ "node_modules/csstype": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
+ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -1793,6 +1857,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -1801,6 +1873,15 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "9.0.19",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz",
+ "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -2735,6 +2816,65 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-redux": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
+ "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8 || ^17.0 || ^18.0",
+ "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react-native": ">=0.59",
+ "redux": "^4"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "peerDependencies": {
+ "redux": "^4"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@@ -2756,6 +2896,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/reselect": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
+ "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
+ },
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -3206,6 +3351,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/client/package.json b/client/package.json
index f3c6a72..81fa310 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,10 +15,12 @@
"export": "npm run build && next export"
},
"dependencies": {
+ "@reduxjs/toolkit": "^1.9.3",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"next": "13.2.4",
"react": "18.2.0",
- "react-dom": "18.2.0"
+ "react-dom": "18.2.0",
+ "react-redux": "^8.0.5"
}
}
diff --git a/client/src/common/layout/head/index.js b/client/src/common/layout/head/index.js
new file mode 100644
index 0000000..13a2683
--- /dev/null
+++ b/client/src/common/layout/head/index.js
@@ -0,0 +1,12 @@
+import Head from 'next/head'
+
+export default function HeadComponent () {
+ return (
+
+ React Hooks Playground
+
+
+
+
+ )
+}
diff --git a/client/src/common/layout/page/index.js b/client/src/common/layout/page/index.js
new file mode 100644
index 0000000..5980e0e
--- /dev/null
+++ b/client/src/common/layout/page/index.js
@@ -0,0 +1,13 @@
+import HeadComponent from '../head'
+
+export default function Page ({ children }) {
+ return (
+
+
+ { children }
+
+ )
+}
diff --git a/client/src/common/ui/card/Card.module.css b/client/src/common/ui/card/Card.module.css
new file mode 100644
index 0000000..7f38a9c
--- /dev/null
+++ b/client/src/common/ui/card/Card.module.css
@@ -0,0 +1,8 @@
+.card {
+ border: 1px solid;
+ min-width: 400px;
+ max-width: 600px;
+ min-height: 100px;
+ border-radius: 16px;
+ padding: 16px;
+}
\ No newline at end of file
diff --git a/client/src/common/ui/card/index.js b/client/src/common/ui/card/index.js
new file mode 100644
index 0000000..d1d9b1e
--- /dev/null
+++ b/client/src/common/ui/card/index.js
@@ -0,0 +1,11 @@
+import styles from './Card.module.css'
+
+function Card ({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Card
diff --git a/client/src/styles/Home.module.css b/client/src/components/home/Home.module.css
similarity index 97%
rename from client/src/styles/Home.module.css
rename to client/src/components/home/Home.module.css
index 27dfff5..8073c1c 100644
--- a/client/src/styles/Home.module.css
+++ b/client/src/components/home/Home.module.css
@@ -1,10 +1,10 @@
.main {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
+
+ height: 100vh;
+ display: grid;
+ place-content: center;
padding: 6rem;
- min-height: 100vh;
+
}
.description {
@@ -16,6 +16,7 @@
width: 100%;
z-index: 2;
font-family: var(--font-mono);
+ margin: auto;
}
.description a {
@@ -72,6 +73,11 @@
max-width: 30ch;
}
+.h2 {
+ text-align: center;
+ margin-bottom: 24px;
+}
+
.center {
display: flex;
justify-content: center;
diff --git a/client/src/components/home/index.js b/client/src/components/home/index.js
new file mode 100644
index 0000000..b6c20cc
--- /dev/null
+++ b/client/src/components/home/index.js
@@ -0,0 +1,30 @@
+import Link from 'next/link'
+import Page from '@/common/layout/page'
+
+import { Inter } from 'next/font/google'
+import styles from './Home.module.css'
+import navlinks from './items.json'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export default function HomeComponent() {
+ return (
+
+
+
+
+
+ React Hooks Playground
+
+
+
+ {navlinks.map((item, index) => (
+
+ {item.name}
+
+ ))}
+
+
+
+ )
+}
diff --git a/client/src/components/home/items.json b/client/src/components/home/items.json
new file mode 100644
index 0000000..9deab84
--- /dev/null
+++ b/client/src/components/home/items.json
@@ -0,0 +1,14 @@
+[
+ {
+ "name": "useSyncExternalStore",
+ "link": "/usesyncexternalstore"
+ },
+ {
+ "name": "redux toolkit",
+ "link": "/redux"
+ },
+ {
+ "name": "useState",
+ "link": "/usestate"
+ }
+]
\ No newline at end of file
diff --git a/client/src/components/redux/index.js b/client/src/components/redux/index.js
new file mode 100644
index 0000000..0a154f1
--- /dev/null
+++ b/client/src/components/redux/index.js
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types'
+import Page from '@/common/layout/page'
+import Card from '@/common/ui/card'
+import TodoListComponent from '@/domain/redux/todolist'
+
+function ReduxComponent ({
+ addTodo,
+ deleteTodo
+}) {
+ return (
+
+
+ Redux Toolkit
+
+
+ Testing page re-renders and data rendering from a redux store inside a deeply-nested component.
+
+
+
+
+
+ {/** A deeply-nested component */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hello
+
+
+ )
+}
+
+ReduxComponent.propTypes = {
+ addTodo: PropTypes.func,
+ deleteTodo: PropTypes.func
+}
+
+export default ReduxComponent
diff --git a/client/src/components/usestate/index.js b/client/src/components/usestate/index.js
new file mode 100644
index 0000000..9b7c353
--- /dev/null
+++ b/client/src/components/usestate/index.js
@@ -0,0 +1,85 @@
+import { useState } from 'react'
+import Card from '@/common/ui/card'
+import Page from '@/common/layout/page'
+
+import TodoListComponentV3 from '@/domain/usestate/todolist'
+import TodoListComponentFull from '@/domain/usestate/todolistfull'
+
+function UseStateComponent () {
+ const [state, setState] = useState([])
+
+ const addTodo = () => {
+ const data = [...state]
+ data.push({
+ id: Math.random().toString(36).substring(2, 8),
+ text: 'Hello, wooorld!!'
+ })
+
+ setState(data)
+ }
+
+ const deleteTodo = (id) => {
+ const temp = state.filter(item => item.id !== id)
+ setState(temp)
+ }
+
+ return (
+
+
+ useState
+
+
+ Testing page re-renders and local state set by useState rendering from inside a deeply-nested component.
+
+
+
+
+
+ ToDo list state passed from props
+
+
+ {/** Renders a list of ToDo items passed from props */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ToDo list state isolated on an inner component
+
+
+ {/** Renders a list of ToDo items with all local state inside the TodoListComponentFull component */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default UseStateComponent
diff --git a/client/src/components/usesyncexternalstore/index.js b/client/src/components/usesyncexternalstore/index.js
new file mode 100644
index 0000000..5e7687b
--- /dev/null
+++ b/client/src/components/usesyncexternalstore/index.js
@@ -0,0 +1,57 @@
+import PropTypes from 'prop-types'
+import Page from '@/common/layout/page'
+import Card from '@/common/ui/card'
+import TodoListComponentV2 from '@/domain/usesyncexternalstore/todolist'
+
+function UseSyncExternalStoreComponent ({
+ addTodo,
+ deleteTodo
+}) {
+ return (
+
+
+ useSyncExternalStore
+
+
+
+ Testing page re-renders and data rendering from a global variable set by useSyncExternalStore inside a deeply-nested component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello
+
+
+ )
+}
+
+UseSyncExternalStoreComponent.propTypes = {
+ addTodo: PropTypes.func,
+ deleteTodo: PropTypes.func
+}
+
+export default UseSyncExternalStoreComponent
diff --git a/client/src/domain/redux/todolist/index.js b/client/src/domain/redux/todolist/index.js
new file mode 100644
index 0000000..c382d22
--- /dev/null
+++ b/client/src/domain/redux/todolist/index.js
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+
+function TodoListComponent ({ deleteTodo }) {
+ const {ids, entities: todos } = useSelector(state => state.todos)
+
+ return (
+
+ {(ids).map(((id, index) => (
+ -
+ id: {todos[id].id}, {todos[id].text}
+
+
+
+
+ )))}
+
+ )
+}
+
+TodoListComponent.propTypes = {
+ deleteTodo: PropTypes.func
+}
+
+export default TodoListComponent
diff --git a/client/src/domain/usestate/todolist/index.js b/client/src/domain/usestate/todolist/index.js
new file mode 100644
index 0000000..29949ee
--- /dev/null
+++ b/client/src/domain/usestate/todolist/index.js
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types'
+
+function TodoListComponentV3 ({
+ todos,
+ deleteTodo
+}) {
+ return (
+
+ {(todos).map(((item, index) => (
+ -
+ id: {item.id}, {item.text}
+
+
+
+
+ )))}
+
+ )
+}
+
+TodoListComponentV3.propTypes = {
+ todos: PropTypes.array,
+ deleteTodo: PropTypes.func
+}
+
+export default TodoListComponentV3
diff --git a/client/src/domain/usestate/todolistfull/index.js b/client/src/domain/usestate/todolistfull/index.js
new file mode 100644
index 0000000..aad705a
--- /dev/null
+++ b/client/src/domain/usestate/todolistfull/index.js
@@ -0,0 +1,44 @@
+import { useState } from 'react'
+
+function TodoListComponentFull () {
+ const [todos, setState] = useState([])
+
+ const addTodo = () => {
+ const data = [...todos]
+ data.push({
+ id: Math.random().toString(36).substring(2, 8),
+ text: 'Hi, wooorld!!'
+ })
+
+ setState(data)
+ }
+
+ const deleteTodo = (id) => {
+ const temp = todos.filter(item => item.id !== id)
+ setState(temp)
+ }
+
+ return (
+ <>
+
+
+
+
+ {(todos).map(((item, index) => (
+ -
+ id: {item.id}, {item.text}
+
+
+
+
+ )))}
+
+ >
+ )
+}
+
+export default TodoListComponentFull
diff --git a/client/src/domain/usesyncexternalstore/todolist/index.js b/client/src/domain/usesyncexternalstore/todolist/index.js
new file mode 100644
index 0000000..6b85bfd
--- /dev/null
+++ b/client/src/domain/usesyncexternalstore/todolist/index.js
@@ -0,0 +1,29 @@
+import PropTypes from 'prop-types'
+import useTodos from '@/lib/hooks/usetodo'
+
+function TodoListComponentV2 ({
+ deleteTodo
+}) {
+ const { todos } = useTodos()
+
+ return (
+
+ {(todos).map(((item, index) => (
+ -
+ id: {item.id}, {item.text}
+
+
+
+
+ )))}
+
+ )
+}
+
+TodoListComponentV2.propTypes = {
+ deleteTodo: PropTypes.func
+}
+
+export default TodoListComponentV2
diff --git a/client/src/lib/hooks/usetodo.js b/client/src/lib/hooks/usetodo.js
new file mode 100644
index 0000000..756752a
--- /dev/null
+++ b/client/src/lib/hooks/usetodo.js
@@ -0,0 +1,74 @@
+import { useSyncExternalStore } from 'react'
+
+// https://react.dev/reference/react/useSyncExternalStore
+
+// Global item id
+let nextId = 0
+
+// Global store containing an array of objects
+let todos = [{ id: nextId++, text: 'Todo #1' }]
+
+// Internal listeners for each method in todoStore initialized by useSyncExternalStore.
+// Needs to call emitChange() to take effect
+let listeners = []
+
+/**
+ * Exportable hook that uses useSyncExternalStore and a global variable to store data.
+ * Usage: const { todos, addTodo, deleteTodo } = useTodos()
+ * @returns {Object} { todos, addTodo, deleteTodo }
+ */
+export default function useTodos () {
+ const todos = useSyncExternalStore(
+ todoStore.subscribe,
+ todoStore.getSnapshot,
+ todoStore.getServerSnapshot
+ )
+
+ return {
+ todos,
+ addTodo: todoStore.addTodo,
+ deleteTodo: todoStore.deleteTodo
+ }
+}
+
+/**
+ * The external store with available useSyncExternalStore-required methods:
+ * subscribe()
+ * - Used and initialized internally by useSyncExternalStore for subscribing to external store events.
+ * - This should return a clean-up function like in useEffect() with empty dependency arrays.
+ * getSnapshot() - Returns the current (full) snapshot of the global data variable
+ * getServerSnapshot() - Used in SSR
+ */
+export const todoStore = {
+ addTodo () {
+ todos = [ ...todos, { id: nextId++, text: 'Todo #' + nextId }]
+ emitChange()
+ },
+
+ deleteTodo (id) {
+ todos = todos.filter(item => item.id !== id)
+ emitChange()
+ },
+
+ subscribe (listener) {
+ listeners = [...listeners, listener]
+
+ return () => {
+ listeners = listeners.filter(item => item !== listener)
+ }
+ },
+
+ getSnapshot () {
+ return todos
+ },
+
+ getServerSnapshot () {
+ return todos
+ }
+}
+
+function emitChange () {
+ for (let listener of listeners) {
+ listener()
+ }
+}
diff --git a/client/src/lib/store/store.js b/client/src/lib/store/store.js
new file mode 100644
index 0000000..8e86db4
--- /dev/null
+++ b/client/src/lib/store/store.js
@@ -0,0 +1,18 @@
+import { combineReducers } from 'redux'
+import { configureStore } from '@reduxjs/toolkit'
+
+import todoSlice from '@/lib/store/todos/todoSlice'
+
+// Reducers
+const combinedReducer = combineReducers({
+ todos: todoSlice
+})
+
+const rootReducer = (state, action) => {
+ return combinedReducer(state, action)
+}
+
+// Global store
+export const store = configureStore({
+ reducer: rootReducer
+})
diff --git a/client/src/lib/store/todos/todoSlice.js b/client/src/lib/store/todos/todoSlice.js
new file mode 100644
index 0000000..dc1f51a
--- /dev/null
+++ b/client/src/lib/store/todos/todoSlice.js
@@ -0,0 +1,54 @@
+// Notes:
+// https://redux.js.org/tutorials/essentials/part-6-performance-normalization#normalized-state-structure
+// https://redux.js.org/tutorials/essentials/part-6-performance-normalization#optimizing-the-posts-list
+
+import {
+ createSlice,
+ createEntityAdapter
+} from '@reduxjs/toolkit'
+
+const STATES = {
+ IDLE: 'idle',
+ PENDING: 'pending'
+}
+
+// Entiti adapter
+const todosAdapter = createEntityAdapter({
+ selectId: (todo) => todo.id
+})
+
+// Slice
+const todoSlice = createSlice({
+ name: 'todos',
+ initialState: todosAdapter.getInitialState({
+ loading: STATES.IDLE,
+ error: '',
+ success: '',
+ todo: null
+ }),
+ reducers: {
+ todoReceived (state, action) {
+ const id = Math.random().toString(36).substring(2, 8)
+
+ state.loading = STATES.IDLE
+ state.todo = { ...action.payload, id }
+ todosAdapter.addOne(state, state.todo)
+
+ },
+ todoDelete (state, action) {
+ todosAdapter.removeOne(state, action.payload)
+ },
+ todosReceived (state, action) {
+ state.loading = STATES.IDLE
+ todosAdapter.setAll(state, action.payload)
+ }
+ }
+})
+
+export const {
+ todoReceived,
+ todosReceived,
+ todoDelete
+} = todoSlice.actions
+
+export default todoSlice.reducer
diff --git a/client/src/pages/_app.js b/client/src/pages/_app.js
index 2300201..bf9ec3f 100644
--- a/client/src/pages/_app.js
+++ b/client/src/pages/_app.js
@@ -1,5 +1,12 @@
+import { Provider } from 'react-redux'
+import { store } from '@/store/store'
+
import '@/styles/globals.css'
export default function App({ Component, pageProps }) {
- return
+ return (
+
+
+
+ )
}
diff --git a/client/src/pages/index.js b/client/src/pages/index.js
index 36e2dd1..5ca4058 100644
--- a/client/src/pages/index.js
+++ b/client/src/pages/index.js
@@ -1,123 +1,5 @@
-import Head from 'next/head'
-import Image from 'next/image'
-import { Inter } from 'next/font/google'
-import styles from '@/styles/Home.module.css'
-
-const inter = Inter({ subsets: ['latin'] })
+import HomeComponent from '@/components/home'
export default function Home() {
- return (
- <>
-
- Create Next App
-
-
-
-
-
-
-
- Get started by editing
- src/pages/index.js
-
-
-
-
-
-
-
-
- >
- )
+ return ()
}
diff --git a/client/src/pages/redux/index.js b/client/src/pages/redux/index.js
new file mode 100644
index 0000000..d25077b
--- /dev/null
+++ b/client/src/pages/redux/index.js
@@ -0,0 +1,27 @@
+import { useDispatch } from 'react-redux'
+import { todoReceived, todoDelete } from '@/lib/store/todos/todoSlice'
+
+import ReduxComponent from '@/components/redux'
+
+function ReduxContainer () {
+ const dispatch = useDispatch()
+
+ const addTodo = () => {
+ dispatch(todoReceived({
+ text: 'Hello, world!'
+ }))
+ }
+
+ const deleteTodo = (id) => {
+ dispatch(todoDelete(id))
+ }
+
+ return (
+
+ )
+}
+
+export default ReduxContainer
diff --git a/client/src/pages/usestate/index.js b/client/src/pages/usestate/index.js
new file mode 100644
index 0000000..c9fa64c
--- /dev/null
+++ b/client/src/pages/usestate/index.js
@@ -0,0 +1,9 @@
+import UseStateComponent from '@/components/usestate'
+
+function UseStateContainer () {
+ return (
+
+ )
+}
+
+export default UseStateContainer
diff --git a/client/src/pages/usesyncexternalstore/index.js b/client/src/pages/usesyncexternalstore/index.js
new file mode 100644
index 0000000..078356c
--- /dev/null
+++ b/client/src/pages/usesyncexternalstore/index.js
@@ -0,0 +1,15 @@
+import UseSyncExternalStoreComponent from '@/components/usesyncexternalstore'
+import useTodos from '@/lib/hooks/usetodo'
+
+function UseSyncExternalStore () {
+ const { addTodo, deleteTodo } = useTodos()
+
+ return (
+
+ )
+}
+
+export default UseSyncExternalStore
diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css
index d4f491e..7d2317e 100644
--- a/client/src/styles/globals.css
+++ b/client/src/styles/globals.css
@@ -1,3 +1,7 @@
+html, body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
:root {
--max-width: 1100px;
--border-radius: 12px;
@@ -87,12 +91,6 @@ body {
body {
color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
}
a {