├── .gitignore ├── 00-boilerplate ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 01-hola-redux-antiguo ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── actions │ │ ├── base.actions.ts │ │ ├── index.ts │ │ └── user-profile.actions.ts │ ├── index.css │ ├── main.tsx │ ├── reducers │ │ ├── index.ts │ │ └── user-profile.reducer.ts │ ├── user-profile │ │ ├── index.ts │ │ ├── user-profile.component.tsx │ │ └── user-profile.container.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 02-asincronia-antiguo ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── actions │ │ ├── base.actions.ts │ │ ├── index.ts │ │ ├── member.actions.ts │ │ └── user-profile.actions.ts │ ├── api │ │ └── github.api.ts │ ├── index.css │ ├── main.tsx │ ├── member-list │ │ ├── member-list.component.module.css │ │ ├── member-list.component.tsx │ │ └── member-list.container.tsx │ ├── model │ │ ├── github-member.model.ts │ │ └── index.ts │ ├── reducers │ │ ├── index.ts │ │ ├── members.reducer.ts │ │ └── user-profile.reducer.ts │ ├── user-profile │ │ ├── index.ts │ │ ├── user-profile.component.tsx │ │ └── user-profile.container.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 03-redux-toolkit ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── app-store │ │ └── store.ts │ ├── features │ │ ├── github-members │ │ │ ├── github-members-component.tsx │ │ │ ├── github-members.api.ts │ │ │ ├── github-members.component.module.css │ │ │ ├── github-members.model.ts │ │ │ └── github-members.slice.ts │ │ └── user-profile │ │ │ ├── user-profile.component.tsx │ │ │ └── user-profile.slice.ts │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | /mongo-data 4 | 5 | node_modules 6 | 7 | # compiled output 8 | /dist 9 | /tmp 10 | /out-tsc 11 | # Only exists if Bazel was run 12 | /bazel-out 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # profiling files 18 | chrome-profiler-events*.json 19 | speed-measure-plugin*.json 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | .history/* 37 | 38 | # misc 39 | /.sass-cache 40 | /connect.lock 41 | /coverage 42 | /libpeerconnection.log 43 | npm-debug.log 44 | yarn-error.log 45 | testem.log 46 | .awcache 47 | test-report.* 48 | junit.xml 49 | *.log 50 | *.orig 51 | /typings 52 | report.**.json 53 | 54 | # System Files 55 | .DS_Store 56 | Thumbs.db 57 | 58 | .env 59 | 60 | __azurite_db_queue__.json 61 | __azurite_db_queue_extent__.json 62 | -------------------------------------------------------------------------------- /00-boilerplate/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /00-boilerplate/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /00-boilerplate/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /00-boilerplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /00-boilerplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "00-boilerplate", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.15", 18 | "@types/react-dom": "^18.2.7", 19 | "@typescript-eslint/eslint-plugin": "^6.0.0", 20 | "@typescript-eslint/parser": "^6.0.0", 21 | "@vitejs/plugin-react": "^4.0.3", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.3", 25 | "typescript": "^5.0.2", 26 | "vite": "^4.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /00-boilerplate/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /00-boilerplate/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /00-boilerplate/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | 3 | function App() { 4 | return ( 5 | <> 6 |
Redux 2023 - Boilerplate
7 |
Aquí van las demos..
8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /00-boilerplate/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /00-boilerplate/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /00-boilerplate/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /00-boilerplate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /00-boilerplate/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /00-boilerplate/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/README.md: -------------------------------------------------------------------------------- 1 | # Hola Redux Antiguo 2 | 3 | ## Intro 4 | 5 | Seguramente si estás buscando como loco información acerca de Redux, es que hayas caído en un mantenimiento, y no usen la última versión de Redux, ni Redux Toolkit. 6 | 7 | Así que vamos a arrancar por crear un proyecto a la antigua y comentar que es cada cosa. 8 | 9 | # Pasos 10 | 11 | - Copiate el proyecto de la carpeta _00-boilerplate_ y ejecuta: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | ```bash 18 | npm run dev 19 | ``` 20 | 21 | En el terminal te indicará en que puerto se va a lanzar la aplicación de ejemplo (normalmente _localhost:5173_). 22 | 23 | ## Instalando Redux 24 | 25 | En proyecto antiguos seguramente tengas instalado Redux y los Typings de Redux, tenías que hacer custro install (el de redux y el de @types/redux y el de react-redux y sus typings). 26 | 27 | Si te instalas redux ahora verás que no hace falta. 28 | 29 | ```bash 30 | npm install redux react-redux --save 31 | ``` 32 | 33 | > ¿Por qué redux y react-redux? Redux es la implementación en JavaScript puro, y react-redux toma como base ésta y le añade la fontanería para que funciona con React. 34 | 35 | Volvemos a ejecutar nuestro server local: 36 | 37 | ```bash 38 | npm run dev 39 | ``` 40 | 41 | ## El ejemplo mínimo 42 | 43 | Vamos a montar un ejemplo mínimo (que sería un overkill...), vamos a mostrar un nombre de usuario y también vamos a dejar que el usuario lo edite. 44 | 45 | ### Paso 1 creando el store 46 | 47 | En Redux el estado de la aplicación se guarda en un objeto llamado _store_, para crearlo tenemos que usar la función _createStore_. 48 | 49 | Peeero... la cosa no es tan fácil como _crea el store y fuera_ el estado lo vamos a dividir en islas separadas y cada isla va a tener una función que llamaremos _reducer_ que nos permitirá actualizar el estado de esa isla. 50 | 51 | > Esa función se llama _reducer_ porque acepta dos parámetros (la foto del estado que había hasta ese momento, y la acción que se quiere ejecutar) y devuelve un nuevo estado (es decir reduce de dos parámetros de entrada a uno de salida). 52 | 53 | Así que queremos almacenar el nombre del usuario en un estado, ¿Qué vamos a hacer? 54 | 55 | - Crear un interfaz que defina ese estado. 56 | - Usarlo en un reducer 57 | - Añadir al store (es decir al almacén de estados). 58 | 59 | Para verlo paso a paso vamos crear una estructura de carpetas sencilla (en un proyecto real habría que plantear otra). 60 | 61 | _./src/reducers/user-profile.reducer.ts_ 62 | 63 | ```typescript 64 | export interface UserProfileState { 65 | name: string; 66 | } 67 | 68 | export const createDefaultUserProfile = (): UserProfileState => ({ 69 | name: "Sin nombre", 70 | }); 71 | ``` 72 | 73 | Es decir aquí: 74 | 75 | - Definimos un trozo de estado de la aplicación (interfaz UserProfileState). 76 | - También definimos un un método que nos permite crear un estado por defecto (createDefaultUserProfile). 77 | 78 | Vamos ahora a por el reducer (en el mismo fichero): 79 | 80 | _./src/reducers/user-profile.reducer.ts_ 81 | 82 | **Añadir al final** 83 | 84 | ```tsx 85 | export const userProfileReducer = ( 86 | state: UserProfileState = createDefaultUserProfile(), 87 | action: any 88 | ) => { 89 | return state; 90 | }; 91 | ``` 92 | 93 | > Ojo aquí nos avisa TS que _action_ no lo estamos usando, de momento podemos vivir con eso, si no, podemos renombrar _action_ y poner un \_\_\_ para que no nos de el warning. 94 | 95 | ¿Qué estamos haciendo aquí? De momento poca cosa 96 | 97 | - Recibimos el estado actual de la aplicación, que si de primeras es null o undefined, llamar a _createDefaultUserProfile_ para crear un estado por defecto (que va a contener en el campo _name_ el valor _Sin nombre_). 98 | - Devolvemos justo el mismo estado. 99 | 100 | > ¿Por qué hacemos esto? Porque en Redux no se puede devolver un estado null o undefined, siempre tiene que haber un estado, y ... ¿Cómo lo modificamos? Ya lo aprenderemos más adelante. 101 | 102 | Siguiente paso, ya tenemos un trozo de estado y una función que nos servirá para almacenar la información, tenemos que guardarlo todo en el gran almacén de nuestra aplicación (el _Store_ de redux). 103 | 104 | _./src/reducers/index.ts_ 105 | 106 | ```typescript 107 | import { combineReducers } from "redux"; 108 | import { UserProfileState, userProfileReducer } from "./user-profile.reducer"; 109 | 110 | export interface AppState { 111 | userProfile: UserProfileState; 112 | } 113 | 114 | export const rootReducer = combineReducers({ 115 | userProfile: userProfileReducer, 116 | }); 117 | ``` 118 | 119 | ¿Qué estamos haciendo aquí? 120 | 121 | - Creamos un interfaz que nos servirá para definir el estado de la aplicación (AppState), ahí podemos guardar un montón de trozos de estado, por ejemplo el perfil de usuario, el carrito de la compra, etc... 122 | 123 | - Creamos un reducer que nos servirá para actualizar el estado de la aplicación (rootReducer), en este caso sólo tenemos un trozo de estado, pero podríamos tener muchos más. 124 | 125 | - En ese reducer de momento va a estar nuestro _userProfileReducer_. 126 | 127 | > Si te fijas lo hemos creado en el fichero _index.ts_ así a la hora de importarlo sólo tenemos que referenciar la carpeta _reducers_. 128 | 129 | ### Creando el store 130 | 131 | Ya tenemos todos los reducers y trozos de estado listo, nos queda crear el gran almacén (el store), y engancharlo en el ciclo de vida de React (para ello definimos un _Provider_ en el nivel más alto de la aplicación, así desde cualquier punto de la aplicación podremos acceder al mismo). 132 | 133 | Para ello en _./src/app.tsx_ lo definimos: 134 | 135 | _./src/app.tsx_ 136 | 137 | ```diff 138 | import "./App.css"; 139 | + import { createStore } from "redux"; 140 | + import { Provider } from "react-redux"; 141 | + import { rootReducer } from "./reducers"; 142 | 143 | + const store = createStore(rootReducer); 144 | 145 | function App() { 146 | return ( 147 | <> 148 | + 149 |
Redux 2023 - Boilerplate
150 |
Aquí van las demos..
151 | +
152 | 153 | ); 154 | } 155 | 156 | export default App; 157 | ``` 158 | 159 | > Aquí te aparecerá que _createStore_ está deprecated, pero es que estamos usando la versión antigua de Redux, así que no te preocupes (después usaremos la moderna) 160 | 161 | Si ejecutamos podemos ver que... bueno el código no peta pero no hay nada :D, primera lección de Redux, vas a picar mucho código sin ver resultados :D. 162 | 163 | Antes de meternos a hablar de acciones, contenedores y hooks, vamos a insertar unas devtools que nos permitirán ver el estado de la aplicación en cada momento (así al menos podemos ver que se ha enganchado todo correctamente). 164 | 165 | Para ello tienes que instalarte la extensión de redux dev tools en tu navegador favorito. 166 | 167 | Para Chrome la puedes encontrar aquí: 168 | 169 | https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=es 170 | 171 | Si volvemos a ejecutar... tampoco se ve nada, Ooops, hay que hacer un paso de fontanería para enlazarlo 172 | 173 | Te presento a los _middlewares_ que con extensiones que puedes añadir a Redux para enseñarle trucos nuevos, ¿Cómo funcionan? Se meten en medio del flujo de redux y las acciones y demás pasan por ellos, estos middlewares pueden o bien dejar pasar la acción, o bien modificarla, o bien hacer algo con ella. 174 | 175 | Para configurar esto (en modo antiguo) con las dev tools haremos lo siguiente: 176 | 177 | _./src/App.tsx_ 178 | 179 | ```diff 180 | import "./App.css"; 181 | - import { createStore } from "redux"; 182 | + import { compose, createStore } from "redux"; 183 | import { Provider } from "react-redux"; 184 | import { rootReducer } from "./reducers"; 185 | 186 | + // TypeScript: https://www.mydatahack.com/getting-redux-devtools-to-work-with-typescript/ 187 | + declare global { 188 | + interface Window { 189 | + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; 190 | + } 191 | + } 192 | + 193 | + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 194 | + 195 | const store = createStore(rootReducer 196 | + ,composeEnhancers() 197 | ); 198 | ``` 199 | 200 | ¿Qué estamos haciendo aquí? Tenemos que liarla un poco más de la cuenta para cumplir con los tipos y el tipo estricto de TypeScript (si no estás en modo estricto, esta solución te vale también) 201 | 202 | - Primero, vamos a acceder al objeto global _Windows_ ¿Qué pasa? Que ese objeto en TypeScript no tiene una llamada _REDUX_DEVTOOLS_EXTENSION_COMPOSE_ así que tenemos que declararla (y decirle que es una función que devuelve el tipo de _compose_ de redux). 203 | 204 | - Segundo chequeamos si existe (si no están las redux dev tools instaladas nos devuelve undefined), así que si lo está usamos el compose de las devtools, si no el compose que trae redux. 205 | 206 | - Después añadimos ese middleware al store. 207 | 208 | - Primero comprobar si están instaladas las _devtools_ de redux (si lo están hay una variable global en el objeto _window_ del navegador que se llama '**REDUX_DEVTOOLS_EXTENSION**'. 209 | 210 | - En caso de que exista la añadimos como middleware a nuestro store (de ahí que lo invoquemos como función). 211 | 212 | > A tener en cuenta: en producción igual no queremos que esto aparezca, así que te hará falta en un proyecto real comprobar si estás haciendo build para producción o desarrollo para activarlo o desactivarlo (como curiosidad aplicaciones como _slack_ o _bitbucket_ estaban por defecto conectadas a las devtools en producción). 213 | 214 | Si ejecutamos ahora, veremos que en la pestaña de Redux de las devtools aparece un estado inicial de la aplicación. 215 | 216 | ```bash 217 | npm run dev 218 | ``` 219 | 220 | ### Contenedores y componentes 221 | 222 | Ahora estarás pensando... _muy bonito, lo veo en las devtools, pero yo quiero mostrar el nombre por pantalla..._ vamos a seguir con redux _antiguo_ y a crear unos contenederos que nos permitan acceder al estado de la aplicación. 223 | 224 | ¿Esto que es? 225 | 226 | - Tengo redux y un store con el estado de la aplicación. 227 | - ¿Cómo accedo al mismo? 228 | - Con redux antiguo usando un container que me permite conectar estado y acciones (las acciones las veremos luego). 229 | - Con redux moderno, existen los hooks que nos permiten acceder al estado y a las acciones (quizás si esto hubieses existido de primeras nos habría hecho la vida más fácil :)) 230 | 231 | Así que lo que vamos a hacer es crear lo siguiente: 232 | 233 | - Una carpeta en la que almacenaremos todo nuestro código relacionado con editar un perfil (interfaz de usuario). 234 | - Un contenedor que conecte los datos que tenemos en el store y se lo pase al componente hijo que será el que presente la información. 235 | - Un componente que tenga la lógica para mostrar el nombre del usuario (más adelante permitiremos editar el nombre). 236 | 237 | Vamos a ello: 238 | 239 | Primero el componente presentacional: 240 | 241 | _./src/user-profile/user-profile.component.tsx_ 242 | 243 | ```typescript 244 | import React from "react"; 245 | 246 | export interface UserProfileProps { 247 | name: string; 248 | } 249 | 250 | export const UserProfileComponent: React.FC = ({ name }) => { 251 | return ( 252 | <> 253 |

User Profile

254 |
255 | 256 | {name} 257 |
258 | 259 | ); 260 | }; 261 | ``` 262 | 263 | ¿Qué hacemos aquí? 264 | 265 | - Le decimos que esperamos que el padre nos de una propiedad _name_ (el nombre del usuario) y la mostramos). 266 | - Fíjate que aquí no hay nada de redux, ni store, ni nada, es un componente que recibe una propiedad y la muestra. 267 | - Puedes pensar en las pelís de Narcos, este componente es el "pringao" que no conoce nada del negocio, sólo sabe que le pasan mercancia y la vende. 268 | 269 | Vamos ahora a por el contenedor: 270 | 271 | - En Redux antiguo se utilizaba la función _connect_ que nos permitía conectar el estado y las acciones con el componente. 272 | - Este _connect_ es un High Order Component (HOC), si no sabes lo que es eso, te has librado de una buena :), si estás en un proyecto muy antiguo de React, aquí puedes estudiarlo: https://legacy.reactjs.org/docs/higher-order-components.html 273 | - Aquí le decimos que del estado del store, nos quedamos con la propiedad _name_ y se la pasamos al componente _UserProfileComponent_. 274 | 275 | _./src/user-profile/user-profile.container.tsx_ 276 | 277 | ```typescript 278 | import { connect } from "react-redux"; 279 | import { UserProfileComponent } from "./user-profile.component"; 280 | import { AppState } from "../reducers"; 281 | 282 | const mapStateToProps = (state: AppState) => ({ 283 | name: state.userProfile.name, 284 | }); 285 | 286 | export const UserProfileContainer = 287 | connect(mapStateToProps)(UserProfileComponent); 288 | ``` 289 | 290 | _./src/user-profile/index.ts_ 291 | 292 | ```typescript 293 | export * from "./user-profile.container"; 294 | ``` 295 | 296 | Vamos ahora a usar ese contenedor en nuestro componente principal: 297 | 298 | _./src/App.tsx_ 299 | 300 | ```diff 301 | import { createStore, compose } from "redux"; 302 | import { Provider } from "react-redux"; 303 | import { rootReducer } from "./reducers"; 304 | + import { UserProfileContainer } from "./user-profile"; 305 | 306 | // TypeScript: https://www.mydatahack.com/getting-redux-devtools-to-work-with-typescript/ 307 | declare global { 308 | interface Window { 309 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; 310 | } 311 | } 312 | 313 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 314 | 315 | const store = createStore(rootReducer, composeEnhancers()); 316 | 317 | function App() { 318 | return ( 319 | <> 320 | 321 |
Redux 2023 - Boilerplate
322 | -
Aquí van las demos..
323 | +
324 | + 325 | +
326 |
327 | 328 | ); 329 | } 330 | ``` 331 | 332 | Vamos a verlo en funcionamiento. 333 | 334 | ¿Y para esto tanto lío? Bueno si... esto sólo le verás beneficios en cierto tipo de aplicaciones, en teoría lo que ganamos: 335 | 336 | - Separamos estado de interfaz de usuario. 337 | - Tenemos un sólo sitio donde se almacenan los datos de la aplicación. 338 | 339 | ... Bueno y más cosas bonitas que después veremos que en la práctica hace que tengas un desarrollo pesado (salvo que estés en un escenario donde el patrón te aporte, pista... una aplicación de gestión de toda la vida no suele ser buen sitio para usar Redux)... en cuanto hayamos completado este tutorial básico, nos mojaremos y pondremos algunos ejemplos de aplicaciones en lo que si que puede tener sentido usar Redux. 340 | 341 | ### Acciones y dispatch 342 | 343 | Ya sabemos como mostrar datos en pantalla, pero... ¿Cómo los modificamos? 344 | 345 | Ahora vienen las acciones y el dispatch ¿Qué es esto? 346 | 347 | Empecemos por las acciones: 348 | 349 | - Cuando quiero modificar un dato, soy un tipo ordenador y defino una _acción_ es acción no es otra cosa que un interfaz con una estructura, suele tener dos campos: 350 | - El ID de la acción, así sabemos que vamos a hacer (por ejemplo _UPDATE_USER_NAME_). 351 | - El payload de la acción, es decir que valores tengo que incluir para modificar el estado (por ejemplo el nuevo nombre del usuario), este payload puede tener cualquier estructura, por ejemplo podría ser la ficha de un cliente. 352 | 353 | Todo esto está muy bien ¿Pero como meto una acción en el flujo de Redux? 354 | 355 | Vamos a utilizar una analogía para explicar esto: 356 | 357 | - Imagínate que quieres ir la trabajo. 358 | - La empresa te pones una "lanzadera" (Que no deja ser un autobus que va recogiendo empleados y los deja en la puerta de la oficina). 359 | - Esa lanzadera te recoge y te lleva hasta allí... ala a currar como un campeón 360 | 361 | Pues bien el pobre diablo que va a trabajar es la acción. 362 | 363 | El autobus que va recogiendo trabajadores es el dispatcher. 364 | 365 | Así pues vamos a definir un acción que nos permita modificar el nombre del usuario: 366 | 367 | Primero definimos un tipo base para las acciones (de momento tiramos de _any_ para el payload, después tiraremos más info ¿Por qué _any_ porque en una aplicación tendremos multiples acciones y cada una tendrá un payload diferente, así que no podemos definir un tipo base para todas... bueno después veremos un truco). 368 | 369 | _./src/actions/base.actions.ts_ 370 | 371 | ```typescript 372 | export interface BaseAction { 373 | type: string; 374 | payload: any; // Aupa el any !! 375 | } 376 | ``` 377 | 378 | Y ahora la acción que nos permite modificar el nombre del usuario, vamos a usar una función que nos ayude a crearla (a esta función tan tonta se le da el rimbombante nombre de _action creator_). 379 | 380 | _./src/actions/user-profile.actions.ts_ 381 | 382 | ```typescript 383 | import { BaseAction } from "./base.actions"; 384 | 385 | export const UPDATE_USER_NAME = "[USER_PROFILE] Update user name"; 386 | 387 | export const createUpdateUserNameAction = (name: string): BaseAction => ({ 388 | type: UPDATE_USER_NAME, 389 | payload: name, 390 | }); 391 | ``` 392 | 393 | Vamos a crear un barrel para agrupar todo esto 394 | 395 | _./src/actions/index.ts_ 396 | 397 | ```typescript 398 | export * from "./base.actions"; 399 | export * from "./user-profile.actions"; 400 | ``` 401 | 402 | Ya tenemos la acción, fíjate que le hemos puesto un prefijo ¿Por qué? Porque estas acciones pasan por todos los reducers, y si no ponemos un prefijo, podríamos tener colisiones de nombres. 403 | 404 | Vamos ahora a usarla en nuestro reducer: 405 | 406 | _./src/reducers/user-profile.reducer.ts_ 407 | 408 | ```diff 409 | + import { BaseAction, UPDATE_USER_NAME } from "../actions"; 410 | 411 | export interface UserProfileState { 412 | name: string; 413 | } 414 | 415 | export const createDefaultUserProfile = (): UserProfileState => ({ 416 | name: "Sin nombre", 417 | }); 418 | 419 | export const userProfileReducer = ( 420 | state: UserProfileState = createDefaultUserProfile(), 421 | - action: any 422 | + action: BaseAction 423 | ) => { 424 | 425 | + switch (action.type) { 426 | + case UPDATE_USER_NAME: 427 | + return { 428 | + ...state, 429 | + name: action.payload, 430 | + }; 431 | + } 432 | 433 | return state; 434 | }; 435 | ``` 436 | 437 | ¿Qué estamos haciendo aquí? 438 | 439 | - Me viene una acción y pasa por el reducer (aquí tenemos un switch). 440 | - ¿Qué la acción no es la que espero? Pues devuelvo el estado tal cual. 441 | - ¿Qué la acción es la que espero? Pues devuelvo el estado actualizado con el nuevo nombre. 442 | 443 | Si te fijas esta actualización es inmutable, es decir lo que era el estado actual no lo modifico (no lo muto), si no que creo un nuevo objeto, varias cosas a tener en cuenta sobre esto: 444 | 445 | - Nos complicamos la vida, y en algunos casos es un auténtico tostón actualizar el estado de manera inmutable. 446 | - Ganamos mucho en que nuestro código es más robusto y mantenible (un cambio en un objeto no afecta a otro sin avisar). 447 | - Si no te suena lo que son _los tres puntitos_ (spread operator), te toca ponerte a estudiar ES6: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Operadores/Spread_operator 448 | - La recomendación actual es utilizar una librería como _immer_ para gestionar la inmutabilidad: https://immerjs.github.io/immer/docs/introduction 449 | 450 | Otro _palabro_ más para terminar _un reducer_ se dice que es una _función pura_ ¿Esto que es? 451 | 452 | Una función que no tiene efectos secundarios, es decir que no modifica nada fuera de ella, sólo devuelve un valor en función de los parámetros de entrada. 453 | 454 | ¿Qué NO son funciones puras? 455 | 456 | - Una función que utilice una variable que éste definida fuera de ella. 457 | - Una función qu lee de una API Rest. 458 | - Una función que genera números aleatorios. 459 | 460 | Lo que se busca es algo determinista, los efectos colaterales (side effects) aportan caos e intentamos aislaros en otro sitio. 461 | 462 | Un último ajuste, en un reducer lo normal es sacar cada caso en una función aparte, algo así como: 463 | 464 | _./src/reducers/user-profile.reducer.ts_ 465 | 466 | ```diff 467 | 468 | + const handleUpdateUserName = ( 469 | + state: UserProfileState, 470 | + name: string 471 | + ): UserProfileState => ({ 472 | + ...state, 473 | + name, 474 | + }); 475 | 476 | export interface UserProfileState { 477 | name: string; 478 | } 479 | 480 | export const createDefaultUserProfile = (): UserProfileState => ({ 481 | name: "Sin nombre", 482 | }); 483 | 484 | export const userProfileReducer = ( 485 | state: UserProfileState = createDefaultUserProfile(), 486 | action: BaseAction 487 | ) => { 488 | switch (action.type) { 489 | case UPDATE_USER_NAME: 490 | - return { 491 | - ...state, 492 | - name: action.payload, 493 | - }; 494 | + return handleUpdateUserName(state, action.payload); 495 | } 496 | 497 | return state; 498 | }; 499 | ``` 500 | 501 | Ahora toca exponer esa acción al componente para que pueda modificar el estado de la aplicación. 502 | 503 | ¿Te acuerdas del mapDispatchToProps? Pues eso es lo que vamos a hacer: 504 | 505 | _./src/user-profile/user-profile.container.tsx_ 506 | 507 | ```diff 508 | import { connect } from "react-redux"; 509 | import { UserProfileComponent } from "./user-profile.component"; 510 | import { AppState } from "../reducers"; 511 | + import { BaseAction, createUpdateUserNameAction } from "../actions"; 512 | + import { Dispatch } from "redux"; 513 | 514 | const mapStateToProps = (state: AppState) => ({ 515 | name: state.userProfile.name, 516 | }); 517 | 518 | + const mapDispatchToProps = (dispatch: Dispatch) => ({ 519 | + onUpdateUserName: (name: string) => dispatch(createUpdateUserNameAction(name)), 520 | + }); 521 | 522 | export const UserProfileContainer = 523 | - connect(mapStateToProps)(UserProfileComponent); 524 | + connect(mapStateToProps, mapDispatchToProps)(UserProfileComponent); 525 | ``` 526 | 527 | ¿Qué hacemos aquí? ... ¿Te acuerdas el famoso dispatcher? Pues en _mapDispatchToProps_ tenemos acceso la mismo, y lo que hacemos es definir una función (que después el componente presentacional la tendrá como _prop_) en la que recibimos el nuevo nombre, lo empaquetamos en la acción de _createUpdateUserNameAction_ y se lo pasamos al dispatcher, el dispatcher se va a encargar de pasearlo por todos los reducers para que vean si es su parada y actualicen estado. 528 | 529 | En el connect añadimos como segundo parámetro el _mapDispatchToProps_. 530 | 531 | > Como curiosidad el tercer parámetro de connect es _mergeProps_ que nos permite mezclar las propiedades que vienen del estado y las que vienen de las acciones (si ves algo de esto en código legacy es que se metieron en un buen berenjenal) 532 | 533 | Así que ahora vamos a actualizarlo en el componente, a destacar aquí, como en las props y los narcos, el componente no sabe nada de redux, sólo sabe que le pasan una propiedad _onUpdateUserName_ que es una función que recibe un nombre y lo actualiza. 534 | 535 | _./src/user-profile/user-profile.component.tsx_ 536 | 537 | ```diff 538 | import React from "react"; 539 | 540 | export interface UserProfileProps { 541 | name: string; 542 | + onUpdateUserName: (name: string) => void; 543 | } 544 | 545 | 546 | - export const UserProfileComponent: React.FC = ({ name }) => { 547 | + export const UserProfileComponent: React.FC = ({ name, onUpdateUserName }) => { 548 | return ( 549 | <> 550 |

User Profile

551 |
552 | 553 | {name} 554 | +
555 | + onUpdateUserName(e.target.value)}> 556 |
557 | 558 | ); 559 | }; 560 | ``` 561 | 562 | Y ahora tenemos funcionando este ejemplo. 563 | 564 | Seguramente estarás pensando... la que hemos liado para una etiqueta y un input :), tienes toda la razón, este ejemplo no tendría ningún sentido en la vida real, pero es para que veas como funciona el flujo de Redux, también comentarte en muchos tipos de aplicación tampoco tiene sentido meterse con Redux (cada herramienta tiene su uso, por ejemplo, por muchas ganas que tengas, no te vas a poner a matar moscas con un martillo). 565 | 566 | Antes de seguir un apunte interesante, es muy fácil implementar pruebas unitarias en el: 567 | 568 | - Reducer. 569 | - Acciones. 570 | 571 | Algo más complicado pero viable en: 572 | 573 | - Contenedores. 574 | - Componentes. 575 | 576 | # Tipando 577 | 578 | Hay un sitio donde todo este de redux hace aguas con TypeScript y es en tipar las acciones, eso de meter un _any_ en el payload como un castillo, es un coladero de errores. 579 | 580 | Vamos a ver como podemos tipar esto: 581 | 582 | _./src/actions/base.actions.ts_ 583 | 584 | ```diff 585 | 586 | + export enum ActionTypes { 587 | + UPDATE_USER_NAME = "[USER_PROFILE] Update user name", 588 | + } 589 | + 590 | + export type BaseAction = 591 | + | { 592 | + type: ActionTypes.UPDATE_USER_NAME; 593 | + payload: string; 594 | + }; 595 | 596 | - export interface BaseAction { 597 | - type: string; 598 | - payload: any; // Aupa el any !! 599 | - } 600 | ``` 601 | 602 | Y ahora en la acción: 603 | 604 | _./src/actions/user-profile.actions.ts_ 605 | 606 | ```diff 607 | - import { BaseAction } from "./base.actions"; 608 | + import { ActionTypes, BaseAction } from "./base.actions"; 609 | - 610 | - export const UPDATE_USER_NAME = "[USER_PROFILE] Update user name"; 611 | 612 | export const createUpdateUserNameAction = (name: string): BaseAction => ({ 613 | - type: UPDATE_USER_NAME, 614 | + type: ActionTypes.UPDATE_USER_NAME, 615 | payload: name, 616 | }); 617 | ``` 618 | 619 | En el reducer 620 | 621 | _./src/reducers/user-profile.reducer.ts_ 622 | 623 | ```diff 624 | - import { BaseAction, UPDATE_USER_NAME } from "../actions"; 625 | + import { BaseAction, ActionTypes } from "../actions"; 626 | 627 | const handleUpdateUserName = ( 628 | state: UserProfileState, 629 | name: string 630 | ): UserProfileState => ({ 631 | ...state, 632 | name, 633 | }); 634 | 635 | export interface UserProfileState { 636 | name: string; 637 | } 638 | 639 | export const createDefaultUserProfile = (): UserProfileState => ({ 640 | name: "Sin nombre", 641 | }); 642 | 643 | export const userProfileReducer = ( 644 | state: UserProfileState = createDefaultUserProfile(), 645 | action: BaseAction 646 | ) => { 647 | switch (action.type) { 648 | - case UPDATE_USER_NAME: 649 | + case ActionTypes.UPDATE_USER_NAME: 650 | 651 | ``` 652 | 653 | Y ahora en el reducer una vez que nos metemos en el case del switch el payload se tipa como string. 654 | 655 | Si quieres práctica un poco, podrías probar a meter en ese reducer un apellido y montar toda la fontanería :). 656 | 657 | A tener en cuenta: 658 | 659 | - Lo normal en una aplicación real es que tengas varios reducers. 660 | - Cada reducer tiene su propio estado y ésta aislado (no se puede habar directamente con reducers hermanos) 661 | - Para comunicar reducers se utilizan acciones (que se pasan por todos los reducers). 662 | - Si necesitas combinar datos de dos reducers, o bien los pides en el mapStateToProps o bien puedes usar campos calculados, había una librería que se llamaba [reselect](https://github.com/reduxjs/reselect) que si la están usando en el proyecto legacy que has entrado te tocará estudiar (es potente pero requiere su tiempo de estudio), o también en redux moderno tienes algo parecido ya incorporado. 663 | 664 | # Maquina del tiempo y devtools 665 | 666 | Ahora viene el momento _vendida de moto_ de Redux, vamos a ver como podemos viajar en el tiempo y ver como estaba la aplicación en un momento determinado. 667 | 668 | Prueba a abrir las dev tools, juega a rellenar varias veces el text box, y ahora dale al play, ahí lo tienes puede ver que hizo el usuario paso a paso, incluso puedes borrar acciones, o añadir nuevas. 669 | 670 | Esto es una pasada, de hecho un juego de tablero online, lo utilizamos de la siguiente manera: 671 | 672 | - El usuario empieza partida. 673 | - La aplicación peta 674 | - Recibimos como parte del log la reproducción completa de los pasos que dió el usuario (eliminábamos antes datos sensible como la clave del usuario). 675 | - Así podíamos reproducir paso a paso que hizo el jugador. 676 | 677 | ¿Esto parece chulo verdad? Pues el precio que te toca pagar es alto: 678 | 679 | - Tienes que tener todo el estado de tu aplicación en Redux (esto ya te aconsejan que no lo hagas salvo causa justificada). 680 | - Había que conectar el router spa (react-router) con redux, esto se pudo hacer durante un tiempo, hasta que el equipo de React Router dijo que no era buena idea, y que no lo iban a mantener. 681 | 682 | Asi que bueno... esta chulo, pero no es todo lo práctico que debería. 683 | 684 | # Sobre estructura de carpetas 685 | 686 | La estructura de carpetas que hemos usado, sólo sirve para aprender como va _redux_ en un proyecto pequeño, es lo que se llama dividir una carpeta por tipo de elemento: 687 | 688 | Aquí va el árbol de directorio 689 | 690 | ``` 691 | ├── actions 692 | │ ├── base.actions.ts 693 | │ └── user-profile.actions.ts 694 | ├── reducers 695 | │ ├── index.ts 696 | │ └── user-profile.reducer.ts 697 | ├── user-profile 698 | │ ├── index.ts 699 | │ ├── user-profile.component.tsx 700 | │ └── user-profile.container.tsx 701 | ├── App.css 702 | ├── App.tsx 703 | ├── index.tsx 704 | ``` 705 | 706 | pero en un proyecto real, lo normal es que tengas una estructura de carpetas por funcionalidad, es decir: 707 | 708 | ``` 709 | ├── core 710 | │ ├── base.actions.ts 711 | │ ├── root.store.ts 712 | │ ├── common.actions.ts 713 | │ ├── common.reducers.ts 714 | ├── pods 715 | │ ├── profile 716 | │ | ├── profile.actions.ts 717 | │ | ├── profile.reducers.ts 718 | │ | ├── profile.component.ts 719 | │ | ├── profile.container.ts 720 | ``` 721 | 722 | Y bueno aquí te puedes encontrar _fumadas_ de todos los colores, estudia bien como está todo organizado en tu proyecto legacy e intenta ser consistente cuando actualices código. 723 | 724 | # Curiosidades 725 | 726 | Redux está fuertemente inspirado en ELM, un lenguaje funcional que se ejecuta en el navegador, si te interesa el tema, puedes ver este vídeo: https://www.youtube.com/watch?v=NYb2GDWMIm0 727 | 728 | ¿Por qué se hizo tan popular Redux? Porque en React no había forma de manejar datos globales, el React Context apareció luego (al menos la versión estable) 729 | 730 | Aplicaciones en las que puede tener sentido usar Redux: 731 | 732 | - En una aplicación de un juego de tablero (ajedrez, dominó...), puedes ser interesante tener el estado en Redux si algo falla, puedes grabar los pasos completos y reproducirlo. 733 | - En una aplicación como Slack (varios canales, actualizaciones, ....) 734 | 735 | Donde no suele tener sentido, en una aplicación de gestión al uso. 736 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "00-boilerplate", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-redux": "^8.1.2", 16 | "redux": "^4.2.1" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.15", 20 | "@types/react-dom": "^18.2.7", 21 | "@typescript-eslint/eslint-plugin": "^6.0.0", 22 | "@typescript-eslint/parser": "^6.0.0", 23 | "@vitejs/plugin-react": "^4.0.3", 24 | "eslint": "^8.45.0", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "eslint-plugin-react-refresh": "^0.4.3", 27 | "typescript": "^5.0.2", 28 | "vite": "^4.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { createStore, compose } from "redux"; 3 | import { Provider } from "react-redux"; 4 | import { rootReducer } from "./reducers"; 5 | import { UserProfileContainer } from "./user-profile"; 6 | 7 | // TypeScript: https://www.mydatahack.com/getting-redux-devtools-to-work-with-typescript/ 8 | declare global { 9 | interface Window { 10 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; 11 | } 12 | } 13 | 14 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 15 | 16 | const store = createStore(rootReducer, composeEnhancers()); 17 | 18 | function App() { 19 | return ( 20 | <> 21 | 22 |
Redux 2023 - Boilerplate
23 | 24 |
25 | 26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/actions/base.actions.ts: -------------------------------------------------------------------------------- 1 | export enum ActionTypes { 2 | UPDATE_USER_NAME = "[USER_PROFILE] Update user name", 3 | } 4 | 5 | export type BaseAction = { 6 | type: ActionTypes.UPDATE_USER_NAME; 7 | payload: string; 8 | }; 9 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base.actions"; 2 | export * from "./user-profile.actions"; 3 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/actions/user-profile.actions.ts: -------------------------------------------------------------------------------- 1 | import { BaseAction, ActionTypes } from "./base.actions"; 2 | 3 | export const createUpdateUserNameAction = (name: string): BaseAction => ({ 4 | type: ActionTypes.UPDATE_USER_NAME, 5 | payload: name, 6 | }); 7 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { UserProfileState, userProfileReducer } from "./user-profile.reducer"; 3 | 4 | export interface AppState { 5 | userProfile: UserProfileState; 6 | } 7 | 8 | export const rootReducer = combineReducers({ 9 | userProfile: userProfileReducer, 10 | }); 11 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/reducers/user-profile.reducer.ts: -------------------------------------------------------------------------------- 1 | import { BaseAction, ActionTypes } from "../actions"; 2 | 3 | const handleUpdateUserName = ( 4 | state: UserProfileState, 5 | name: string 6 | ): UserProfileState => ({ 7 | ...state, 8 | name, 9 | }); 10 | 11 | export interface UserProfileState { 12 | name: string; 13 | } 14 | 15 | export const createDefaultUserProfile = (): UserProfileState => ({ 16 | name: "Sin nombre", 17 | }); 18 | 19 | export const userProfileReducer = ( 20 | state: UserProfileState = createDefaultUserProfile(), 21 | action: BaseAction 22 | ) => { 23 | switch (action.type) { 24 | case ActionTypes.UPDATE_USER_NAME: 25 | return handleUpdateUserName(state, action.payload); 26 | } 27 | 28 | return state; 29 | }; 30 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/user-profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user-profile.container"; 2 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/user-profile/user-profile.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface UserProfileProps { 4 | name: string; 5 | onUpdateUserName: (name: string) => void; 6 | } 7 | 8 | export const UserProfileComponent: React.FC = ({ name, onUpdateUserName }) => { 9 | return ( 10 | <> 11 |

User Profile

12 |
13 | 14 | {name} 15 | onUpdateUserName(e.target.value)}> 16 |
17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/user-profile/user-profile.container.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { UserProfileComponent } from "./user-profile.component"; 3 | import { AppState } from "../reducers"; 4 | import { BaseAction, createUpdateUserNameAction } from "../actions"; 5 | import { Dispatch } from "redux"; 6 | 7 | const mapStateToProps = (state: AppState) => ({ 8 | name: state.userProfile.name, 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 12 | onUpdateUserName: (name: string) => 13 | dispatch(createUpdateUserNameAction(name)), 14 | }); 15 | 16 | export const UserProfileContainer = connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(UserProfileComponent); 20 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /01-hola-redux-antiguo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/README.md: -------------------------------------------------------------------------------- 1 | # Asincronia 2 | 3 | ## Intro 4 | 5 | Hasta ahora todo muy bonito, pero si te fijas la acciones son 100% síncronas, tu 6 | tienes tu _action creator_ (la función de ayuda) y directamente devuelves un objeto que montas corriendo en el bus del dispatcher para que los reducers lo procesen. 7 | 8 | ¿Pero qué pasa si la acción es asíncrona? Algo tan tonto como hacer una llamada a una API Rest y tener que gestionar el resultado de una promesa. 9 | 10 | Aquí podríamos esta tentados de meter en la acción la promesa, y en el reducer esperar a que vuelva, pero aquí nos meteríamos en un berenjenal: los reducers tiene que sere funciones puras, y también lo suyo es que no suelten la hebra de ejecución y den el resultado ipsofacto (si no imaginate el lío que se puede montar si la promesa tarda más en ejecutarse). 11 | 12 | Así que nos tenemos que inventar algo, aquí el gran Dan Abramov se saco una genialidad "Redux Thunk" en 43 líneas de código. 13 | 14 | # Pasos 15 | 16 | Partimos del código del ejemplo anterior, vamos a leer de la API Rest de Github, la lista de miembros que pertenecen a _Lemoncode_ y mostrarla por pantalla... todo pasando por Redux. 17 | 18 | ## API de Github 19 | 20 | Esta parte no tiene que ver nada con Redux, simplemente vamos a hacer una llamada asíncrona contra una API Rest y utilizaremos la librería _axios_ para realizar la llamada. 21 | 22 | Paramos el server local y ejecutamos el siguiente comando: 23 | 24 | ```bash 25 | npm install axios --save 26 | ``` 27 | 28 | Volvemos a arrancar el server local 29 | 30 | ```bash 31 | npm run dev 32 | ``` 33 | 34 | Vamos a implementar la llamada a la API de Github para obtener los miembros de una organización: 35 | 36 | Definimos un modelo: 37 | 38 | _./src/model/github-member.model.ts_ 39 | 40 | ```ts 41 | export interface GithubMemberEntity { 42 | id: number; 43 | login: string; 44 | avatar_url: string; 45 | } 46 | ``` 47 | 48 | Y su barrel: 49 | 50 | _./src/model/index.ts_ 51 | 52 | ```ts 53 | export * from "./github-member.model"; 54 | ``` 55 | 56 | _./src/api/github.api.ts_ 57 | 58 | ```ts 59 | import axios from "axios"; 60 | import { GithubMemberEntity } from "../model/github-member.model"; 61 | 62 | const url = "https://api.github.com/orgs/lemoncode/members"; 63 | 64 | export const getMembers = (): Promise => 65 | axios.get(url).then((response) => response.data); 66 | ``` 67 | 68 | > Ojo, no uses esta estructura de carpetas para un proyecto real, si quieres ver como trabajar con estructuras: https://github.com/Lemoncode/lemon-front-estructura 69 | 70 | ### Redux Thunk 71 | 72 | Vamos a crear una acción que se ejecutará cuando tengamos resuelta la llamada. 73 | 74 | _./src/actions/base.actions.ts_ 75 | 76 | ```diff 77 | + import { GithubMemberEntity } from "../model"; 78 | 79 | export enum ActionTypes { 80 | UPDATE_USER_NAME = "[USER_PROFILE] Update user name", 81 | + FETCH_MEMBERS_COMPLETED = "[MEMBERS] Fetch members completed", 82 | } 83 | 84 | export type BaseAction = 85 | - { 86 | + |{ 87 | type: ActionTypes.UPDATE_USER_NAME; 88 | payload: string; 89 | } 90 | + | { 91 | + type: ActionTypes.FETCH_MEMBERS_COMPLETED; 92 | + payload: GithubMemberEntity[]; 93 | + }; 94 | ``` 95 | 96 | > Aquí es donde empieza a brillar este tipado, lo podremos comprobar en el reducer. 97 | 98 | _./src/actions/members.actions.ts_ 99 | 100 | ```ts 101 | import { GithubMemberEntity } from "../model/github-member.model"; 102 | import { BaseAction, ActionTypes } from "./base.actions"; 103 | 104 | export const fetchMembersCompleted = ( 105 | members: GithubMemberEntity[] 106 | ): BaseAction => ({ 107 | type: ActionTypes.FETCH_MEMBERS_COMPLETED, 108 | payload: members, 109 | }); 110 | ``` 111 | 112 | Lo exportamos en el barrel: 113 | 114 | _./src/actions/index.ts_ 115 | 116 | ```diff 117 | export * from "./base.actions"; 118 | export * from "./user-profile.actions"; 119 | + export * from "./member.actions"; 120 | ``` 121 | 122 | Y ahora el thunk para que invoque a la API y cuando se resuelva a la acción de completado: 123 | 124 | Antes que nada tenemos que instalar el middleware de Thunk: 125 | 126 | ```bash 127 | npm install redux-thunk --save 128 | ``` 129 | 130 | Y configurar redux thunk en el store: 131 | 132 | _./src/App.tsx_ 133 | 134 | ```diff 135 | import "./App.css"; 136 | - import { createStore, compose } from "redux"; 137 | + import { createStore, compose, applyMiddleware } from "redux"; 138 | import { Provider } from "react-redux"; 139 | + import thunk from "redux-thunk"; 140 | import { rootReducer } from "./reducers"; 141 | import { UserProfileContainer } from "./user-profile"; 142 | 143 | 144 | 145 | // TypeScript: https://www.mydatahack.com/getting-redux-devtools-to-work-with-typescript/ 146 | declare global { 147 | interface Window { 148 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; 149 | } 150 | } 151 | 152 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 153 | 154 | - const store = createStore(rootReducer, composeEnhancers()); 155 | + const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk))); 156 | ``` 157 | 158 | > Si, redux thunk es un middleware, a partir de ahora se meterá esnifar cada acción que entre y si detecta que en vez de un objeto, es una función la ejecutará y no la pasará a los reducers (cuando termine su ejecución llamará a la acción de completado con el resultado, y está si pasará a todos los reducers). 159 | 160 | Vamos ahora a definir la acción de Thunk 161 | 162 | _./src/actions/members.actions.ts_ 163 | 164 | ```diff 165 | import { GithubMemberEntity } from "../model/github-member.model"; 166 | + import { getMembers } from "../api/github.api"; 167 | + import { Dispatch } from "redux"; 168 | import { BaseAction, ActionTypes } from "./base.actions"; 169 | 170 | 171 | export const fetchMembersCompleted = (members: GithubMemberEntity[]) => ({ 172 | type: ActionTypes.FETCH_MEMBERS_COMPLETED, 173 | payload: members, 174 | }); 175 | 176 | + export const fetchMembersRequest = () => (dispatch : Dispatch) => { 177 | + getMembers().then((members) => { 178 | + dispatch(fetchMembersCompleted(members)); 179 | + }); 180 | + }; 181 | ``` 182 | 183 | ¿Qué estamos haciendo aquí? Estamos usando una [función currificada](https://es.javascript.info/currying-partials), primero recibe los parametro que pueda recibir el action creator (en este caso ninguno, pero por ejemplo podría ser el ID de un miembo del equipo), y después thunk evaluará si ese action creator ha devuelto una función y la ejecutará, pasándole como parámetro el dispatch (el bus del dispatcher), así puedes cuando se resuelva la promesa de la llamada a la API se ejecutará la acción _fetchMembersCompleted_, esta acción ya es una acción normal y corriente (devuelve un objeto) y ReduxThunk la dejará pasar. 184 | 185 | Un recordatorio, todo esto son piezas pequeñitas, es muy fácil meter una cagada en una y que todo el trenecito deje de funcionar y sea complicado encontrar el error, por ello: 186 | 187 | - Es importante que tipemos todo lo que podemos (a más tipado menos errores). 188 | - Es importante que hagamos tests unitarios (a más tests menos errores). 189 | 190 | ### Reducer 191 | 192 | Cómo esta lista no tiene nada que ver con el perfil del usuario, vamos a crear un nuevo reducer, así vemos como podemos tener varios reducers en el store. 193 | 194 | Primero creamos el reducer, acuérdate que sólo tiene que tratar la acción de fetch completed, porque la de start ni es una acción (es un Thunk), esto tiene la pega de que en las devtools no aparecerá. 195 | 196 | _./src/reducers/members.reducer.ts_ 197 | 198 | ```ts 199 | import { GithubMemberEntity } from "../model/github-member.model"; 200 | import { BaseAction } from "../actions"; 201 | import { ActionTypes } from "../actions"; 202 | 203 | const handleFetchMembersCompleted = ( 204 | _: GithubMemberEntity[], 205 | members: GithubMemberEntity[] 206 | ) => [...members]; 207 | 208 | export const membersReducer = ( 209 | state: GithubMemberEntity[] = [], 210 | action: BaseAction 211 | ) => { 212 | switch (action.type) { 213 | case ActionTypes.FETCH_MEMBERS_COMPLETED: 214 | return handleFetchMembersCompleted(state, action.payload); 215 | } 216 | 217 | return state; 218 | }; 219 | ``` 220 | 221 | > EL \_ es porque no vamos a usar el primer parámetro, pero no podemos dejarlo vacío porque el compilador de TS se quejaría. 222 | 223 | Ya tenemos el reducer, ahora lo tenemos que añadir al rootReducer: 224 | 225 | _./src/reducers/index.ts_ 226 | 227 | ```diff 228 | import { combineReducers } from "redux"; 229 | import { UserProfileState, userProfileReducer } from "./user-profile.reducer"; 230 | + import { GithubMemberEntity } from '../model'; 231 | + import { membersReducer } from "./members.reducer"; 232 | 233 | export interface AppState { 234 | userProfile: UserProfileState; 235 | + members: GithubMemberEntity[]; 236 | } 237 | 238 | export const rootReducer = combineReducers({ 239 | userProfile: userProfileReducer, 240 | + members: membersReducer, 241 | }); 242 | ``` 243 | 244 | Tipado definido y añadido al _rootReducer_. 245 | 246 | ### Componente 247 | 248 | Ahora vamos a crear un componente que muestre la lista de miembros, para ello vamos a crear un componente funcional, que reciba los miembros como parámetro y los muestre por pantalla. 249 | 250 | Vamos a estilarlo: 251 | 252 | _./src/member-list/member-list.component.module.css_ 253 | 254 | ```css 255 | .container { 256 | display: grid; 257 | grid-template-columns: 80px 1fr 3fr; 258 | grid-template-rows: 20px; 259 | grid-auto-rows: 80px; 260 | grid-gap: 10px 5px; 261 | } 262 | 263 | .header { 264 | background-color: #2f4858; 265 | color: white; 266 | font-weight: bold; 267 | } 268 | 269 | .container > img { 270 | width: 80px; 271 | } 272 | ``` 273 | 274 | _./src/member-list/member-list.component.tsx_ 275 | 276 | ```ts 277 | import * as React from "react"; 278 | import { GithubMemberEntity } from "../model"; 279 | import classes from "./member-list.component.module.css"; 280 | 281 | interface Props { 282 | members: GithubMemberEntity[]; 283 | loadMembers: () => void; 284 | } 285 | 286 | export const MemberListComponent = (props: Props) => { 287 | React.useEffect(() => { 288 | props.loadMembers(); 289 | }, []); 290 | 291 | return ( 292 | <> 293 |

Members Page

294 |
295 | Avatar 296 | Id 297 | Name 298 | {props.members.map((member) => ( 299 | 300 | 301 | {member.id} 302 | {member.login} 303 | 304 | ))} 305 |
306 | 307 | ); 308 | }; 309 | ``` 310 | 311 | Vamos a crear ahora el container: 312 | 313 | **AVISO: AQUI NOS VA A PETAR EL TIPADO DEL DISPATCH** 314 | 315 | _./src/member-list/member-list.container.tsx_ 316 | 317 | ```tsx 318 | import { connect } from "react-redux"; 319 | import { Dispatch } from "redux"; 320 | import { AppState } from "../reducers"; 321 | import { MemberListComponent } from "./member-list.component"; 322 | import { BaseAction, fetchMembersRequest } from "../actions"; 323 | 324 | const mapStateToProps = (state: AppState) => ({ 325 | members: state.members, 326 | }); 327 | 328 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 329 | loadMembers: () => dispatch(fetchMembersRequest()), 330 | }); 331 | 332 | export const MemberListContainer = connect( 333 | mapStateToProps, 334 | mapDispatchToProps 335 | )(MemberListComponent); 336 | ``` 337 | 338 | **¡¡ Aquí nos peta el tipado !!** Por supuesto y es que ese dispatch espera una acción y no un thunk hackie :), toca crear un tipo que soporte o dispatch o thunk: 339 | 340 | ```diff 341 | import { connect } from "react-redux"; 342 | import { Dispatch } from "redux"; 343 | import { AppState } from "../reducers"; 344 | import { MemberListComponent } from "./member-list.component"; 345 | import { BaseAction, fetchMembersRequest } from "../actions"; 346 | 347 | const mapStateToProps = (state: AppState) => ({ 348 | members: state.members, 349 | }); 350 | 351 | + // Toma leche con moloko !! 352 | + // Esto habría que darle una vuelta para hacerlo más elegante 353 | + type DispatchWithThunk = Dispatch | ((arg: any) => void); 354 | 355 | - const mapDispatchToProps = (dispatch: Dispatch) => ({ 356 | + const mapDispatchToProps = (dispatch: DispatchWithThunk) => ({ 357 | loadMembers: () => dispatch(fetchMembersRequest()), 358 | }); 359 | ``` 360 | 361 | Exponerlo en un barrel 362 | 363 | _./src/member-list/index.ts_ 364 | 365 | ```ts 366 | export * from "./member-list.container"; 367 | ``` 368 | 369 | Y usarlo en nuestra aplicación 370 | 371 | **./src/App.tsx** 372 | 373 | ```diff 374 | import "./App.css"; 375 | import { createStore, compose } from "redux"; 376 | import { Provider } from "react-redux"; 377 | import { rootReducer } from "./reducers"; 378 | import { UserProfileContainer } from "./user-profile"; 379 | + import { MemberListContainer } from "./member-list"; 380 | 381 | // (...) 382 | 383 | function App() { 384 | return ( 385 | <> 386 | 387 |
Redux 2023 - Boilerplate
388 | 389 | + 390 |
391 | 392 | ); 393 | } 394 | ``` 395 | 396 | Si ahora las devtools te darás cuenta de dos cosas: 397 | 398 | - La acción de fetchMembersRequest no aparece en las devtools, es thunk (una función) y no es acción ninguna. 399 | - Se llama dos veces a la acción _FetchMembersCompleted_, en las ultimas versión de React, en desarrollo los componentes se montan dos veces, te aconsejan que pases de _useEffect []_ para hacer llamadas a API Rest y que uses librerías como React Query o tires de frameworks. 400 | 401 | # Sagas 402 | 403 | Redux Thunk es una especie de _hack_, y para escenario simples va bien, pero hay ciertas pegas: 404 | 405 | - Por un lado lo que inicia el Thunk no es una acción y no aparece en las devtools. 406 | - Por otro lado hay casos avanzados que nos costaría cubrir, flujos más complejos de asincronia, como esperar a que varias acciones terminen, ejecutar varias acciones en secuencia, implementar un debounce. 407 | 408 | Para todo esto hay algo más avanzado: Redux Saga 409 | 410 | Si en tu proyecto legacy estás usando Sagas, puedes estar en dos escenarios: 411 | 412 | - Que lo metieron porque _molaba_ pero en realidad no lo necesitan (Con lo que verás un código repetitivo una y otra vez). 413 | 414 | - Que realmente lo están usando, y ahí te vas a encontrar un nivel de complejidad alto, pero nada que no se pueda sacar, primero te tienes que estudiar bien los generadores de ES6, y después las sagas. 415 | 416 | Aquí tienes un webinar sobre Redux Saga: 417 | 418 | https://www.youtube.com/watch?v=oljsA9pry3Q 419 | 420 | ¡ Buena suerte ! 421 | 422 | # Por qué todo esto se fue al carajo 423 | 424 | Redux es un patrón interesante, que sobre el papel aporta muchas ventajas. 425 | 426 | En cuanto te pones a implementar un proyecto real, te das cuenta de que hay que picar mucho código, y son muchas piezas las que tienes que encajar, y que si no tienes un equipo con un nivel alto de conocimiento, es muy fácil que se te vaya de las manos. 427 | 428 | Por otro lado no todo los proyectos encajan en esta filosofía (overkill) y también programarlo en modo antiguo era muy duro, después salió Redux toolkit y se simplificó mucho la cosa, pero ya era tarde. 429 | 430 | A fecha de hoy tirando de React Context y Prop Drill puedes resolver un montón de aplicaciones sin meterte en estas complejidades. 431 | 432 | Aquí esta la labor del arquitecto de software, saber que tecnología encaja en que proyecto y no usarla porque sí. 433 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "00-boilerplate", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.5.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.1.2", 17 | "redux": "^4.2.1", 18 | "redux-thunk": "^2.4.2" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.2.15", 22 | "@types/react-dom": "^18.2.7", 23 | "@typescript-eslint/eslint-plugin": "^6.0.0", 24 | "@typescript-eslint/parser": "^6.0.0", 25 | "@vitejs/plugin-react": "^4.0.3", 26 | "eslint": "^8.45.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.3", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.4.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { createStore, compose, applyMiddleware } from "redux"; 3 | import { Provider } from "react-redux"; 4 | import thunk from "redux-thunk"; 5 | import { rootReducer } from "./reducers"; 6 | import { UserProfileContainer } from "./user-profile"; 7 | import { MemberListContainer } from "./member-list/member-list.container"; 8 | 9 | // TypeScript: https://www.mydatahack.com/getting-redux-devtools-to-work-with-typescript/ 10 | declare global { 11 | interface Window { 12 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; 13 | } 14 | } 15 | 16 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 17 | 18 | const store = createStore( 19 | rootReducer, 20 | composeEnhancers(applyMiddleware(thunk)) 21 | ); 22 | 23 | function App() { 24 | return ( 25 | <> 26 | 27 |
Redux 2023 - Boilerplate
28 | 29 | 30 |
31 | 32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/actions/base.actions.ts: -------------------------------------------------------------------------------- 1 | import { GithubMemberEntity } from "../model"; 2 | 3 | export enum ActionTypes { 4 | UPDATE_USER_NAME = "[USER_PROFILE] Update user name", 5 | FETCH_MEMBERS_COMPLETED = "[MEMBERS] Fetch members completed", 6 | } 7 | 8 | export type BaseAction = 9 | | { 10 | type: ActionTypes.UPDATE_USER_NAME; 11 | payload: string; 12 | } 13 | | { 14 | type: ActionTypes.FETCH_MEMBERS_COMPLETED; 15 | payload: GithubMemberEntity[]; 16 | }; 17 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base.actions"; 2 | export * from "./user-profile.actions"; 3 | export * from "./member.actions"; 4 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/actions/member.actions.ts: -------------------------------------------------------------------------------- 1 | import { GithubMemberEntity } from "../model/github-member.model"; 2 | import { getMembers } from "../api/github.api"; 3 | import { Dispatch } from "redux"; 4 | import { BaseAction, ActionTypes } from "./base.actions"; 5 | 6 | export const fetchMembersCompleted = ( 7 | members: GithubMemberEntity[] 8 | ): BaseAction => ({ 9 | type: ActionTypes.FETCH_MEMBERS_COMPLETED, 10 | payload: members, 11 | }); 12 | 13 | export const fetchMembersRequest = () => (dispatch: Dispatch) => { 14 | getMembers().then((members) => { 15 | dispatch(fetchMembersCompleted(members)); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/actions/user-profile.actions.ts: -------------------------------------------------------------------------------- 1 | import { BaseAction, ActionTypes } from "./base.actions"; 2 | 3 | export const createUpdateUserNameAction = (name: string): BaseAction => ({ 4 | type: ActionTypes.UPDATE_USER_NAME, 5 | payload: name, 6 | }); 7 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/api/github.api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { GithubMemberEntity } from "../model/github-member.model"; 3 | 4 | const url = "https://api.github.com/orgs/lemoncode/members"; 5 | 6 | export const getMembers = (): Promise => 7 | axios.get(url).then((response) => response.data); 8 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/member-list/member-list.component.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: 80px 1fr 3fr; 4 | grid-template-rows: 20px; 5 | grid-auto-rows: 80px; 6 | grid-gap: 10px 5px; 7 | } 8 | 9 | .header { 10 | background-color: #2f4858; 11 | color: white; 12 | font-weight: bold; 13 | } 14 | 15 | .container > img { 16 | width: 80px; 17 | } 18 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/member-list/member-list.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GithubMemberEntity } from "../model"; 3 | import classes from "./member-list.component.module.css"; 4 | 5 | interface Props { 6 | members: GithubMemberEntity[]; 7 | loadMembers: () => void; 8 | } 9 | 10 | export const MemberListComponent = (props: Props) => { 11 | React.useEffect(() => { 12 | props.loadMembers(); 13 | }, []); 14 | 15 | return ( 16 | <> 17 |

Members Page

18 |
19 | Avatar 20 | Id 21 | Name 22 | {props.members.map((member) => ( 23 | 24 | 25 | {member.id} 26 | {member.login} 27 | 28 | ))} 29 |
30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/member-list/member-list.container.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { Dispatch } from "redux"; 3 | import { AppState } from "../reducers"; 4 | import { MemberListComponent } from "./member-list.component"; 5 | import { BaseAction, fetchMembersRequest } from "../actions"; 6 | 7 | const mapStateToProps = (state: AppState) => ({ 8 | members: state.members, 9 | }); 10 | 11 | // Toma leche con moloko !! 12 | // Esto habría que darle una vuelta para hacerlo más elegante 13 | type DispatchWithThunk = Dispatch | ((arg: any) => void); 14 | 15 | const mapDispatchToProps = (dispatch: DispatchWithThunk) => ({ 16 | loadMembers: () => dispatch(fetchMembersRequest()), 17 | }); 18 | 19 | export const MemberListContainer = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(MemberListComponent); 23 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/model/github-member.model.ts: -------------------------------------------------------------------------------- 1 | export interface GithubMemberEntity { 2 | id: number; 3 | login: string; 4 | avatar_url: string; 5 | } 6 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './github-member.model'; -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { UserProfileState, userProfileReducer } from "./user-profile.reducer"; 3 | import { GithubMemberEntity } from "../model"; 4 | import { membersReducer } from "./members.reducer"; 5 | 6 | export interface AppState { 7 | userProfile: UserProfileState; 8 | members: GithubMemberEntity[]; 9 | } 10 | 11 | export const rootReducer = combineReducers({ 12 | userProfile: userProfileReducer, 13 | members: membersReducer, 14 | }); 15 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/reducers/members.reducer.ts: -------------------------------------------------------------------------------- 1 | import { GithubMemberEntity } from "../model/github-member.model"; 2 | import { BaseAction } from "../actions"; 3 | import { ActionTypes } from "../actions"; 4 | 5 | const handleFetchMembersCompleted = ( 6 | _: GithubMemberEntity[], 7 | members: GithubMemberEntity[] 8 | ) => [...members]; 9 | 10 | export const membersReducer = ( 11 | state: GithubMemberEntity[] = [], 12 | action: BaseAction 13 | ) => { 14 | switch (action.type) { 15 | case ActionTypes.FETCH_MEMBERS_COMPLETED: 16 | return handleFetchMembersCompleted(state, action.payload); 17 | } 18 | 19 | return state; 20 | }; 21 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/reducers/user-profile.reducer.ts: -------------------------------------------------------------------------------- 1 | import { BaseAction, ActionTypes } from "../actions"; 2 | 3 | const handleUpdateUserName = ( 4 | state: UserProfileState, 5 | name: string 6 | ): UserProfileState => ({ 7 | ...state, 8 | name, 9 | }); 10 | 11 | export interface UserProfileState { 12 | name: string; 13 | } 14 | 15 | export const createDefaultUserProfile = (): UserProfileState => ({ 16 | name: "Sin nombre", 17 | }); 18 | 19 | export const userProfileReducer = ( 20 | state: UserProfileState = createDefaultUserProfile(), 21 | action: BaseAction 22 | ) => { 23 | switch (action.type) { 24 | case ActionTypes.UPDATE_USER_NAME: 25 | return handleUpdateUserName(state, action.payload); 26 | } 27 | 28 | return state; 29 | }; 30 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/user-profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user-profile.container"; 2 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/user-profile/user-profile.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface UserProfileProps { 4 | name: string; 5 | onUpdateUserName: (name: string) => void; 6 | } 7 | 8 | export const UserProfileComponent: React.FC = ({ name, onUpdateUserName }) => { 9 | return ( 10 | <> 11 |

User Profile

12 |
13 | 14 | {name} 15 | onUpdateUserName(e.target.value)}> 16 |
17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/user-profile/user-profile.container.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { UserProfileComponent } from "./user-profile.component"; 3 | import { AppState } from "../reducers"; 4 | import { BaseAction, createUpdateUserNameAction } from "../actions"; 5 | import { Dispatch } from "redux"; 6 | 7 | const mapStateToProps = (state: AppState) => ({ 8 | name: state.userProfile.name, 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 12 | onUpdateUserName: (name: string) => 13 | dispatch(createUpdateUserNameAction(name)), 14 | }); 15 | 16 | export const UserProfileContainer = connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(UserProfileComponent); 20 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /02-asincronia-antiguo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /03-redux-toolkit/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /03-redux-toolkit/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /03-redux-toolkit/README.md: -------------------------------------------------------------------------------- 1 | # Redux Toolkit 2 | 3 | Hasta ahora hemos visto como crear una aplicación Redux sin ayudas, esto es un dolor, y el equipo de Redux lo sabe, por eso crearon Redux Toolkit, una librería que nos ayuda a crear aplicaciones Redux de forma más sencilla. 4 | 5 | Tiene una documentación muy buena (tutoriales, fundamentos), para arrancarte te puedes arrancar por la parte de 6 | tutoriales: https://redux.js.org/tutorials/index y hasta una quick start para TypeScript: https://redux.js.org/tutorials/typescript-quick-start 7 | 8 | # Arranque 9 | 10 | Vamos a partir del ejemplo _00 boiler plate_ y vamos a crear un proyecto desde cero con Redux (implementaremos tanto el _userprofile_ como el _members_ de los ejemplos anteriores), pero esta vez usando el toolkit. 11 | 12 | Copiamos e instalamos dependencias: 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | # Instalación 19 | 20 | Aquí instalamos el toolkit: 21 | 22 | ```bash 23 | npm install @reduxjs/toolkit react-redux 24 | ``` 25 | 26 | > También se puede arrancar tirando de plantillas, hay una [plantilla oficial en Vite](https://github.com/reduxjs/redux-templates), y aquí las instrucciones para configurarla: https://redux-toolkit.js.org/introduction/getting-started 27 | 28 | Ya podemos dejar corriendo nuestro servidor local: 29 | 30 | ```bash 31 | npm run dev 32 | ``` 33 | 34 | # Estructura de la aplicación 35 | 36 | Redux toolkit nos da ya una estructura para arrancarnos, el árbol que queda: 37 | 38 | ```bash 39 | ├── src 40 | │ ├── index.tsx // El index de la aplicación 41 | │ ├── App.tsx // El componente App 42 | │ ├── /app-store // Aquí va el store, normalmente suele haber sólo uno 43 | │ │ ├── store.ts 44 | │ ├── /features // Agrupamos por características, así tenemos todo en una isla 45 | │ │ ├── /user-profile 46 | │ │ │ ├── user-profile.component.ts 47 | │ │ │ ├── user-profile.slice.ts 48 | │ │ ├── /github-members 49 | │ │ │ ├── github-members.component.ts 50 | │ │ │ ├── github-members.slice.ts 51 | ``` 52 | 53 | # User Profile slice 54 | 55 | Si te fijas en redux _antiguo_ creábamos por un lado las acciones y por otro los reducers, esto es un asco, porque el 90% de las veces son cosas que están muy cohesionadas, ¿Por qué no agruparlo todo en un mismo sitio? Y para el 5% que sea acciones que tengan que pasar por más de un reducer pues ya nos creamos algo común. 56 | 57 | Vamos entonces a definir el _user-profile.slice.ts_ 58 | 59 | _./src/features/user-profile/user-profile.slice.ts_ 60 | 61 | ```typescript 62 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 63 | 64 | export interface UserProfileState { 65 | name: string; 66 | } 67 | 68 | const initialState: UserProfileState = { 69 | name: "Sin nombre", 70 | }; 71 | 72 | export const userProfileSlice = createSlice({ 73 | name: "userProfile", 74 | initialState, 75 | // Ni switch ni nada, objeto reducer y que hace cada acción 76 | reducers: { 77 | setName: (state, action: PayloadAction) => { 78 | state.name = action.payload; 79 | }, 80 | }, 81 | }); 82 | 83 | // De aquí saco las acciones 84 | export const { setName } = userProfileSlice.actions; 85 | 86 | // Esto es un selector para sacar los datos, OJO aquí nos falta el tipado del RootState (store) 87 | export const selectName = (state: { userProfile: UserProfileState }) => 88 | state.userProfile.name; 89 | 90 | // Y aquí sacamos el reducer 91 | export default userProfileSlice.reducer; 92 | ``` 93 | 94 | # Registro store y app 95 | 96 | Este slice lo vamos a registrar en el store: 97 | 98 | _./src/app-store/store.ts_ 99 | 100 | ```ts 101 | import { configureStore } from "@reduxjs/toolkit"; 102 | import userProfileReducer from "../features/user-profile/user-proflie.slice"; 103 | 104 | export const store = configureStore({ 105 | reducer: { 106 | userProfile: userProfileReducer, 107 | }, 108 | }); 109 | 110 | // Aquí sacamos el tipo de RootState del state con typeof 111 | export type RootState = ReturnType; 112 | // Y aquí lo mismo sacamos el tipo del dispatch y las acciones del typeof del dispatch 113 | export type AppDispatch = typeof store.dispatch; 114 | ``` 115 | 116 | Ahora viene algo que da un poco de grima, vamos a meter una referencia circular para para poder tipar el useSelector, esto es un poco _hack_ pero es lo que hay: 117 | 118 | _./src/features/user-profile/user-profile.slice.tsx_ 119 | 120 | ```diff 121 | + import type { RootState } from "../../app-store/store"; 122 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 123 | 124 | // (...) 125 | // Esto es un selector para sacar los datos, OJO aquí nos falta el tipado del RootState (store) 126 | - export const selectName = (state: { userProfile: UserProfileState }) => 127 | + export const selectName = (state: RootState) => 128 | state.userProfile.name; 129 | 130 | // Y aquí sacamos el reducer 131 | export default userProfileSlice.reducer; 132 | ``` 133 | 134 | Toca registrar ahora el store en la aplicación, esta vez lo hacemos en nuestro _main.tsx_: 135 | 136 | _./src/main.tsx_ 137 | 138 | ```diff 139 | import React from 'react'; 140 | import ReactDOM from 'react-dom/client'; 141 | + import { Provider } from 'react-redux'; 142 | import App from './App.tsx'; 143 | + import {store} from "./app-store/store"; 144 | import './index.css'; 145 | 146 | ReactDOM.createRoot(document.getElementById('root')!).render( 147 | 148 | + 149 | 150 | + 151 | , 152 | ) 153 | ``` 154 | 155 | # User Profile component 156 | 157 | Ahora vamos a crear el componente que va a usar el slice: 158 | 159 | Primero vamos a leer el valor 160 | 161 | _./src/features/user-profile/user-profile.component.tsx_ 162 | 163 | ```typescript 164 | import { useSelector } from "react-redux"; 165 | import { selectName } from "./user-profile.slice"; 166 | 167 | export const UserProfileComponent = () => { 168 | const name = useSelector(selectName); 169 | 170 | return ( 171 |
172 |

User Profile

173 |

Nombre: {name}

174 |
175 | ); 176 | }; 177 | ``` 178 | 179 | Ahora vamos a editarlo: 180 | 181 | _./src/features/user-profile/user-profile.component.tsx_ 182 | 183 | ```diff 184 | import React from "react"; 185 | import { useSelector } from "react-redux"; 186 | - import { selectName } from "./user-profile.slice"; 187 | + import { selectName, setName } from "./user-profile.slice"; 188 | + import { useDispatch } from "react-redux"; 189 | + import { AppDispatch } from "../../app-store/store"; 190 | 191 | export const UserProfileComponent = () => { 192 | const name = useSelector(selectName); 193 | + const dispatch = useDispatch(); 194 | 195 | + const handleNameChange = (event: React.ChangeEvent) => { 196 | + dispatch(setName(event.target.value)); 197 | + }; 198 | 199 | return ( 200 |
201 |

User Profile

202 |

Nombre: {name}

203 | + 204 |
205 | ); 206 | }; 207 | ``` 208 | 209 | Y lo instanciamos a nivel de App: 210 | 211 | _./src/App.tsx_ 212 | 213 | ```diff 214 | import "./App.css"; 215 | + import { UserProfileComponent } from "./features/user-profile/user-profile.component"; 216 | 217 | function App() { 218 | return ( 219 | <> 220 |
Redux 2023 - Boilerplate
221 | -
Aquí van las demos..
222 | +
223 | + 224 | +
225 | 226 | ); 227 | } 228 | 229 | export default App; 230 | ``` 231 | 232 | Vamos a ver que todo funciona y recapitulamos. 233 | 234 | Por si no lo teneemos arrancado 235 | 236 | ```bash 237 | npm run dev 238 | ``` 239 | 240 | # Async - Github Members 241 | 242 | ¿Y los Thunk como funcionan con el nuevo Toolkit? Pues resulta que los trae incorporado. 243 | 244 | ## API 245 | 246 | Esto no tiene que ver con Redux. 247 | 248 | Utilizaremos Axios: 249 | 250 | ```bash 251 | npm install axios 252 | ``` 253 | 254 | Vamos a crear una feature para los miembros de github, y añadir el modelo y la api. 255 | 256 | _./src/features/github-members/github-members.model.ts_ 257 | 258 | ```typescript 259 | export interface GithubMember { 260 | id: number; 261 | login: string; 262 | avatar_url: string; 263 | } 264 | ``` 265 | 266 | Vamos a crear un fichero de api para leer los datos con axios: 267 | 268 | _./src/features/github-members/github-members.api.ts_ 269 | 270 | ```typescript 271 | import axios from "axios"; 272 | import { GithubMember } from "./github-members.model"; 273 | 274 | export const fetchMembers = async () => { 275 | const response = await axios.get( 276 | "https://api.github.com/orgs/lemoncode/members" 277 | ); 278 | return response.data; 279 | }; 280 | ``` 281 | 282 | Vamos a crear un slice para los miembros de github, aquí tenemos un poco de _magia_: 283 | 284 | - Tenenemos una función de ayuda _createAsyncThunk_ en la que le indicamos el nombre base de la acción y la función asíncrona que queremos ejecutar. 285 | - En la parte de reducers, tenemos que añadir una sección de _extraReducers_ en la que le indicamos acciones adicionales a escuchar, en este caso el thunk de _fetchMembersAsync_ tiene 3 acciones asociadas: _pending_, _fulfilled_ y _rejected_ (en las devtools puedes ver la pending y la FullFilled), nos enganchamos a la _fulfilled_ y actualizamos el estado. 286 | 287 | > Fijate que en las devtools sale duplicado, esto es por lo que comentamos anteriormente, que las últimas version de React por defecto ejecutan dos veces el _useEffect_ al montarse el componente (en desarrollo no). 288 | 289 | Para los Thunk hay una función de ayuda que nos permite crearlos 290 | 291 | _./src/features/github-members/github-members.slice.ts_ 292 | 293 | ```typescript 294 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 295 | import { fetchMembers } from "./github-members.api"; 296 | import { GithubMember } from "./github-members.model"; 297 | 298 | export interface GithubMembersState { 299 | members: GithubMember[]; 300 | } 301 | 302 | const initialState: GithubMembersState = { 303 | members: [], 304 | }; 305 | 306 | const SLICE_NAME = "githubMembers"; 307 | 308 | export const githubMembersSlice = createSlice({ 309 | name: SLICE_NAME, 310 | initialState, 311 | reducers: {}, 312 | extraReducers(builder) { 313 | builder.addCase(fetchMembersAsync.fulfilled, (state, action) => { 314 | state.members = action.payload; 315 | }); 316 | }, 317 | }); 318 | 319 | export const {} = githubMembersSlice.actions; 320 | 321 | export const fetchMembersAsync = createAsyncThunk( 322 | `${SLICE_NAME}/fetchMembers`, 323 | async () => { 324 | const members = await fetchMembers(); 325 | return members; 326 | } 327 | ); 328 | 329 | export const selectMembers = (state: { githubMembers: GithubMembersState }) => 330 | state.githubMembers.members; 331 | 332 | export default githubMembersSlice.reducer; 333 | ``` 334 | 335 | Vamos añadir ese slice al store: 336 | 337 | _./src/app-store/store.ts_ 338 | 339 | ```diff 340 | import { configureStore } from "@reduxjs/toolkit"; 341 | import userProfileReducer from "../features/user-profile/user-profile.slice"; 342 | + import githubMembersReducer from "../features/github-members/github-members.slice"; 343 | 344 | export const store = configureStore({ 345 | reducer: { 346 | userProfile: userProfileReducer, 347 | + githubMembers: githubMembersReducer, 348 | }, 349 | }); 350 | 351 | // Aquí sacamos el tipo de RootState del state con typeof 352 | export type RootState = ReturnType; 353 | // Y aquí lo mismo sacamos el tipo del dispatch y las acciones del typeof del dispatch 354 | export type AppDispatch = typeof store.dispatch; 355 | ``` 356 | 357 | Y ahora vamos a crear el componente que va a usar el slice, primero ponemos el estilado: 358 | 359 | _./src/features/github-members/github-members.component.module.css_ 360 | 361 | ```css 362 | .container { 363 | display: grid; 364 | grid-template-columns: 80px 1fr 3fr; 365 | grid-template-rows: 20px; 366 | grid-auto-rows: 80px; 367 | grid-gap: 10px 5px; 368 | } 369 | 370 | .header { 371 | background-color: #2f4858; 372 | color: white; 373 | font-weight: bold; 374 | } 375 | 376 | .container > img { 377 | width: 80px; 378 | } 379 | ``` 380 | 381 | Y ahora el componente: 382 | 383 | _./src/features/github-members/github-members.component.tsx_ 384 | 385 | ```typescript 386 | import React, { useEffect } from "react"; 387 | import { useDispatch, useSelector } from "react-redux"; 388 | import { fetchMembersAsync, selectMembers } from "./github-members.slice"; 389 | import { AppDispatch } from "../../app-store/store"; 390 | import styles from "./github-members.component.module.css"; 391 | 392 | export const GithubMembersComponent = () => { 393 | const members = useSelector(selectMembers); 394 | const dispatch = useDispatch(); 395 | 396 | useEffect(() => { 397 | dispatch(fetchMembersAsync()); 398 | }, []); 399 | 400 | return ( 401 |
402 |

Github Members

403 |
404 |
Avatar
405 |
Id
406 |
Login
407 | {members.map((member) => ( 408 | 409 | avatar 410 |
{member.id}
411 |
{member.login}
412 |
413 | ))} 414 |
415 |
416 | ); 417 | }; 418 | ``` 419 | 420 | Y vamos a usarlo: 421 | 422 | _./src/App.tsx_ 423 | 424 | ```diff 425 | import "./App.css"; 426 | import { UserProfileComponent } from "./features/user-profile/user-profile.component"; 427 | + import { GithubMembersComponent } from "./features/github-members/github-members.component"; 428 | 429 | function App() { 430 | return ( 431 | <> 432 |
Redux 2023 - Boilerplate
433 |
434 | 435 | + 436 |
437 | 438 | ); 439 | } 440 | 441 | export default App; 442 | 443 | ``` 444 | -------------------------------------------------------------------------------- /03-redux-toolkit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /03-redux-toolkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "00-boilerplate", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^1.9.5", 14 | "axios": "^1.5.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-redux": "^8.1.2" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.15", 21 | "@types/react-dom": "^18.2.7", 22 | "@typescript-eslint/eslint-plugin": "^6.0.0", 23 | "@typescript-eslint/parser": "^6.0.0", 24 | "@vitejs/plugin-react": "^4.0.3", 25 | "eslint": "^8.45.0", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-react-refresh": "^0.4.3", 28 | "typescript": "^5.0.2", 29 | "vite": "^4.4.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-redux-toolkit/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { UserProfileComponent } from "./features/user-profile/user-profile.component"; 3 | import { GithubMembersComponent } from "./features/github-members/github-members-component"; 4 | 5 | function App() { 6 | return ( 7 | <> 8 |
Redux 2023 - Boilerplate
9 |
10 | 11 | 12 |
13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/app-store/store.ts: -------------------------------------------------------------------------------- 1 | import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; 2 | import userProfileReducer from "../features/user-profile/user-profile.slice"; 3 | import githubMembersReducer from "../features/github-members/github-members.slice"; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | userProfile: userProfileReducer, 8 | githubMembers: githubMembersReducer, 9 | }, 10 | }); 11 | 12 | // Aquí sacamos el tipo de RootState del state con typeof 13 | export type RootState = ReturnType; 14 | // Y aquí lo mismo sacamos el tipo del dispatch y las acciones del typeof del dispatch 15 | export type AppDispatch = typeof store.dispatch; 16 | 17 | export type AppThunk = ThunkAction< 18 | ReturnType, 19 | RootState, 20 | unknown, 21 | Action 22 | >; 23 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/github-members/github-members-component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { fetchMembersAsync, selectMembers } from "./github-members.slice"; 4 | import styles from "./github-members.component.module.css"; 5 | import { AppDispatch } from "../../app-store/store"; 6 | 7 | export const GithubMembersComponent = () => { 8 | const members = useSelector(selectMembers); 9 | const dispatch = useDispatch(); 10 | 11 | useEffect(() => { 12 | const fetchData = async () => { 13 | await dispatch(fetchMembersAsync()); 14 | }; 15 | 16 | fetchData(); 17 | }, []); 18 | 19 | return ( 20 |
21 |

Github Members

22 |
23 |
Avatar
24 |
Id
25 |
Login
26 | {members.map((member) => ( 27 | 28 | avatar 29 |
{member.id}
30 |
{member.login}
31 |
32 | ))} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/github-members/github-members.api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { GithubMember } from "./github-members.model"; 3 | 4 | export const fetchMembers = async () => { 5 | const response = await axios.get( 6 | "https://api.github.com/orgs/lemoncode/members" 7 | ); 8 | return response.data; 9 | }; 10 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/github-members/github-members.component.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: 80px 1fr 3fr; 4 | grid-template-rows: 20px; 5 | grid-auto-rows: 80px; 6 | grid-gap: 10px 5px; 7 | } 8 | 9 | .header { 10 | background-color: #2f4858; 11 | color: white; 12 | font-weight: bold; 13 | } 14 | 15 | .container > img { 16 | width: 80px; 17 | } 18 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/github-members/github-members.model.ts: -------------------------------------------------------------------------------- 1 | export interface GithubMember { 2 | id: number; 3 | login: string; 4 | avatar_url: string; 5 | } 6 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/github-members/github-members.slice.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { fetchMembers } from "./github-members.api"; 3 | import { GithubMember } from "./github-members.model"; 4 | 5 | export interface GithubMembersState { 6 | members: GithubMember[]; 7 | } 8 | 9 | const initialState: GithubMembersState = { 10 | members: [], 11 | }; 12 | 13 | const SLICE_NAME = "githubMembers"; 14 | 15 | export const githubMembersSlice = createSlice({ 16 | name: SLICE_NAME, 17 | initialState, 18 | reducers: {}, 19 | extraReducers(builder) { 20 | builder.addCase(fetchMembersAsync.fulfilled, (state, action) => { 21 | state.members = action.payload; 22 | }); 23 | }, 24 | }); 25 | 26 | export const {} = githubMembersSlice.actions; 27 | 28 | export const fetchMembersAsync = createAsyncThunk( 29 | `${SLICE_NAME}/fetchMembers`, 30 | async () => { 31 | const members = await fetchMembers(); 32 | return members; 33 | } 34 | ); 35 | 36 | export const selectMembers = (state: { githubMembers: GithubMembersState }) => 37 | state.githubMembers.members; 38 | 39 | export default githubMembersSlice.reducer; 40 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/user-profile/user-profile.component.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { selectName, setName } from "./user-profile.slice"; 3 | import { useDispatch } from "react-redux"; 4 | import { AppDispatch } from "../../app-store/store"; 5 | 6 | export const UserProfileComponent = () => { 7 | const name = useSelector(selectName); 8 | const dispatch = useDispatch(); 9 | 10 | const handleNameChange = (event: React.ChangeEvent) => { 11 | dispatch(setName(event.target.value)); 12 | }; 13 | 14 | return ( 15 |
16 |

User Profile

17 |

Nombre: {name}

18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/features/user-profile/user-profile.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import type { RootState } from "../../app-store/store"; 3 | 4 | export interface UserProfileState { 5 | name: string; 6 | } 7 | 8 | const initialState: UserProfileState = { 9 | name: "Sin nombre", 10 | }; 11 | 12 | export const userProfileSlice = createSlice({ 13 | name: "userProfile", 14 | initialState, 15 | // Ni switch ni nada, objeto reducer y que hace cada acción 16 | reducers: { 17 | setName: (state, action: PayloadAction) => { 18 | state.name = action.payload; 19 | }, 20 | }, 21 | }); 22 | 23 | // De aquí saco las acciones 24 | export const { setName } = userProfileSlice.actions; 25 | 26 | // Esto es un selector para sacar los datos, OJO aquí nos falta el tipado del RootState (store) 27 | export const selectName = (state: RootState) => state.userProfile.name; 28 | 29 | // Y aquí sacamos el reducer 30 | export default userProfileSlice.reducer; 31 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Provider } from "react-redux"; 4 | import App from "./App.tsx"; 5 | import { store } from "./app-store/store"; 6 | import "./index.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /03-redux-toolkit/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /03-redux-toolkit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /03-redux-toolkit/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /03-redux-toolkit/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lemoncode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux 2023 2 | 3 | ## ℹ️ ¿Qué es esto? 4 | 5 | Redux fue una tecnología muy popular, entre 2016 y 2019, después cayó en desuso ya que se utilizo para escenarios donde no aplicaba. 6 | 7 | Si has llegado aquí es porque: 8 | 9 | - Seguramente te haya tocado un mantenimiento (¡Buena suerte compañero!). 10 | - Igual has encontrado un proyecto en el que puede aplicar. 11 | 12 | ¿Qué te puedes encontrar en este repo? Unas demos básicas de como se utilizaba Redux en 2016-2019 (seguramente el proyecto en el que hayas caído), y otras utilizando Redux Toolkit 2019-... (seguramente no estés usando esto en el proyecto y no te dejen). 13 | 14 | Son demos muy básicas, **cada una de las demos tiene una guía paso a paso (readme.md) para que las puedes hacer desde cero**, también se incluyen explicaciones y referencias a librerías adicionales que se usaban en su día. 15 | 16 | ## 🌋 ¿Por qué un proyecto Redux dicen que es un infierno? 17 | 18 | Redux y Redux Toolkit son soluciones buenas para escenarios concretos. 19 | 20 | ¿Qué paso entre 2016-2019? Pues que al principio React no tenía maduro su contexto, y los desarrolladores nos _flipamos_ mucho con Redux y lo usamos para escenarios que no aportaban nada, más bien al revés... metíamos un montón de complejidad y código en el proyecto para resolver problemas sencillos. 21 | 22 | > Un martillo es una herramienta estupenda... siempre y cuando no la utilices para matar moscas. 23 | 24 | ¿Qué pasa ahora? 25 | 26 | - Qué hay bases de código enormes que están escritas con Redux. 27 | - Que estás bases de código ya no son fáciles de migrar, ni si quiera a Redux Toolkit. 28 | - Que encima por ese código han pasado muchos desarrolladores que no conocían como funcionaba la librería y han dejado unos pufos considerables. 29 | - Que un desarrollador con la seniority suficiente como para poder gestionar ese código... no va a querer trabajar en ese proyecto y se va a ir a otra empresa (el perfil del _arquitecto_ itinerante, te dejo el pufo y me voy a por el siguiente hype) 30 | 31 | ## 📔 Los ejemplos 32 | 33 | Que ejemplos te puedes encontrar en este repositorio: 34 | 35 | - **00-boilerplate**: este es un punto de partida, una aplicación React que dice eso de "hola mundo". 36 | 37 | - **01-hola-redux-antiguo**: aquí montamos un ejemplo básico con Redux (sin Redux Toolkit), mostramos un nombre y el usuario lo puede cambiar (esto de enlazar el cambio de un input a redux es algo que ahora no se aconseja, pero en el pasado era muy normal, de hecho de llego a usar una librería para gestión de formularios redux-form). 38 | 39 | - **02-asincronia-antiguo**: Este ejemplo parte de 01-hola-redux-antiguo, aquí lo que hacemos es gestionar acciones asíncronas para ello utilizamos la librería redux-thunk, un hack para gestionar asincronia (que por cierto no aparecía en las devtools de redux), también podrás encontrar enlaces con información sobre redux-saga (otra librería para gestionar asincronia, que si la tienes en tu proyecto, preparate para asumir una carga de complejidad, toca empezar por aprender lo que son generadores en JavaScript). 40 | 41 | - **03-redux-toolkit**: Este ejemplo parte de _00-boilerplate_, aquí lo que hacemos es recopilar la funcionalidad del ejemplo 01 y 02 (editar nombre y listado de usuarios de Github) y vemos como se hace esto siguiendo la guía del Toolkit, como verás es menos código, se organiza todo mejor, pero... en algunos momentos nos dará la sensación de que estamos usando magia. 42 | 43 | ## 👣 Pasos a futuro 44 | 45 | Dependiendo del tirón que tenga este repo, estamos planteando montar más ejemplos: 46 | 47 | - Armar los ejemplos de forma que se pueda desarrollar de manera progresiva (ver resultados desde el minuto cero e ir evolucionando). 48 | - Armar un juego sencillo de tablero y o un editor de diagramas sencillo y ver que tal se porta redux (incluyendo patrones como undo/redo, ...) 49 | - Montar un ejemplo con sagas. 50 | 51 | ## 📚 Referencias 52 | 53 | [Redux Toolkit TypeScript](https://redux-toolkit.js.org/usage/usage-with-typescript) 54 | 55 | [Reselect](https://github.com/reduxjs/reselect) 56 | 57 | [Redux Sagas en español](https://www.youtube.com/watch?v=oljsA9pry3Q&t=1s) 58 | 59 | # 👩‍🎓 ¿Te apuntas a nuestro máster? 60 | 61 | Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro [Máster Front End Online Lemoncode](https://lemoncode.net/master-frontend#inicio-banner)? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho. 62 | 63 | También puedes apuntarte a nuestro Bootcamp de Back End [Bootcamp Backend](https://lemoncode.net/bootcamp-backend#inicio-banner) 64 | 65 | Y si tienes ganas de meterte una zambullida en el mundo _devops_ apúntate nuestro [Bootcamp devops online Lemoncode](https://lemoncode.net/bootcamp-devops#bootcamp-devops/inicio) 66 | --------------------------------------------------------------------------------