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 |
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 |
--------------------------------------------------------------------------------