Configuración de Redux para su uso en una aplicación del mundo real

 

 

 

  • Diseño de arquitectura de componentes de interfaz de usuario y tokens, con Nathan Curtis
  • Taller de diseño conductual, con Susan y Guthrie Weinschenk

  • Índice
    1. Presentando Redux
    2. Construyendo diarios.app
      1. Iniciar un proyecto e instalar dependencias
      2. Describir el estado inicial de la aplicación
      3. Configurar la simulación de API con MirageJS
      4. Configurar una tienda Redux

    Redux es una sólida biblioteca de gestión de estado para aplicaciones JavaScript de una sola página. Se describe en la documentación oficial como un contenedor de estado predecible para aplicaciones Javascript y es bastante sencillo aprender los conceptos e implementar Redux en una aplicación sencilla. Sin embargo, pasar de una simple aplicación de contador a una aplicación del mundo real puede ser un gran salto.

     

    Redux es una biblioteca importante en el ecosistema de React y casi la predeterminada para usar cuando se trabaja en aplicaciones de React que involucran administración de estado. Como tal, no se puede subestimar la importancia de saber cómo funciona.

    Esta guía guiará al lector a través de la configuración de Redux en una aplicación React bastante compleja y le presentará la configuración de "mejores prácticas" a lo largo del camino. Será beneficioso especialmente para los principiantes y para cualquiera que quiera llenar los vacíos en su conocimiento de Redux.

    Presentando Redux

    Redux es una biblioteca que tiene como objetivo resolver el problema de la gestión del estado en aplicaciones JavaScript imponiendo restricciones sobre cómo y cuándo pueden ocurrir las actualizaciones de estado. Estas restricciones se forman a partir de los "tres principios" de Redux, que son:

    • Fuente única de verdad
      Todas sus aplicaciones statese guardan en un Redux store. Este estado se puede representar visualmente como un árbol con un único ancestro, y la tienda proporciona métodos para leer el estado actual y suscribirse a cambios desde cualquier lugar dentro de su aplicación.

    • El estado es de sólo lectura.
      La única forma de cambiar el estado es enviar los datos como un objeto simple, llamado acción. Puedes pensar en las acciones como una forma de decirle al estado: "Tengo algunos datos que me gustaría insertar/actualizar/eliminar".

    • Los cambios se realizan con funciones puras.
      Para cambiar el estado de su aplicación, escribe una función que toma el estado anterior y una acción y devuelve un nuevo objeto de estado como el siguiente estado. Esta función se llama reducery es una función pura porque devuelve el mismo resultado para un conjunto determinado de entradas.

       

    El último principio es el más importante en Redux y aquí es donde ocurre la magia de Redux. Las funciones reductoras no deben contener código impredecible ni realizar efectos secundarios como solicitudes de red, y no deben mutar directamente el objeto de estado.

    Redux es una gran herramienta, como aprenderemos más adelante en esta guía, pero no está exenta de desafíos o compensaciones. Para ayudar a que el proceso de escritura de Redux sea eficiente y más agradable, el equipo de Redux ofrece un conjunto de herramientas que resume el proceso de configuración de una tienda Redux y proporciona útiles complementos y utilidades de Redux que ayudan a simplificar el código de la aplicación. Por ejemplo, la biblioteca utiliza Immer.js , una biblioteca que le permite escribir una lógica de actualización inmutable "mutativa", bajo el capó.

    Lectura recomendada : Mejores reductores con Immer

    En esta guía, exploraremos Redux mediante la creación de una aplicación que permite a los usuarios autenticados crear y administrar diarios digitales.

    Construyendo diarios.app

    Como se indicó en la sección anterior, analizaremos más de cerca Redux mediante la creación de una aplicación que permita a los usuarios crear y administrar diarios. Construiremos nuestra aplicación usando React y configuraremos Mirage como nuestro servidor simulado de API, ya que no tendremos acceso a un servidor real en esta guía.

    • Ver código fuente (repositorio de GitHub)

    Iniciar un proyecto e instalar dependencias

    Comencemos con nuestro proyecto. Primero, inicie una nueva aplicación React usando create-react-app:

    Usando npx:

    npx create-react-app diaries-app --template typescript

    Estamos comenzando con la plantilla TypeScript, ya que podemos mejorar nuestra experiencia de desarrollo escribiendo código con seguridad de escritura.

    Ahora, instalemos las dependencias que necesitaremos. Navegue hasta el directorio de su proyecto recién creado

    cd diaries-app

    Y ejecute los siguientes comandos:

    npm install --save redux react-redux @reduxjs/toolkit
    npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
    npm install --save-dev miragejs @types/react-redux @types/react-router-dom @types/yup @types/markdown-to-jsx

    El primer comando instalará Redux, React-Redux (enlaces oficiales de React para Redux) y el kit de herramientas de Redux.

     

    El segundo comando instala algunos paquetes adicionales que serán útiles para la aplicación que crearemos, pero que no son necesarios para funcionar con Redux.

    El último comando instala Mirage y escribe declaraciones para los paquetes que instalamos como devDependencies.

    Describir el estado inicial de la aplicación

    Repasemos en detalle los requisitos de nuestra aplicación. La aplicación permitirá a los usuarios autenticados crear o modificar diarios existentes. Los diarios son privados por defecto, pero pueden hacerse públicos. Finalmente, las entradas del diario se ordenarán por su última fecha de modificación.

    Esta relación debería verse así:

    Una descripción general del modelo de datos de la aplicación. ( Vista previa grande )

    Armados con esta información, ahora podemos modelar el estado de nuestra aplicación. Primero, crearemos una interfaz para cada uno de los siguientes recursos User: Diaryy DiaryEntry. Las interfaces en Typecript describen la forma de un objeto.

    Continúe y cree un nuevo directorio nombrado interfacesen srcel subdirectorio de su aplicación:

    cd src mkdir interfaces

    A continuación, ejecute los siguientes comandos en el directorio que acaba de crear:

    touch entry.interface.tstouch diary.interface.tstouch user.interface.ts

    Esto creará tres archivos llamados Entry.interface.ts , diary.interface.ts y user.interface.ts respectivamente. Prefiero mantener las interfaces que se usarían en varios lugares de mi aplicación en una sola ubicación.

    Abra Entry.interface.ts y agregue el siguiente código para configurar la Entryinterfaz:

    export interface Entry { id?: string; title: string; content: string; createdAt?: string; updatedAt?: string; diaryId?: string;}

    Una entrada de diario típica tendrá un título y algo de contenido, así como información sobre cuándo se creó o se actualizó por última vez. Volveremos a la diaryIdpropiedad más tarde.

    A continuación, agregue lo siguiente a diary.interface.ts :

    export interface Diary { id?: string; title: string; type: 'private' | 'public'; createdAt?: string; updatedAt?: string; userId?: string; entryIds: string[] | null;}

    Aquí tenemos una typepropiedad que espera un valor exacto de "privado" o "público", ya que los diarios deben ser privados o públicos. Cualquier otro valor generará un error en el compilador de TypeScript.

    Ahora podemos describir nuestro Userobjeto en el archivo user.interface.ts de la siguiente manera:

    export interface User { id?: string; username: string; email: string; password?: string; diaryIds: string[] | null;}

    Con nuestras definiciones de tipos terminadas y listas para usarse en nuestra aplicación, configuremos nuestro servidor API simulado usando Mirage.

    Configurar la simulación de API con MirageJS

    Dado que este tutorial se centra en Redux, no entraremos en detalles sobre la configuración y el uso de Mirage en esta sección. Consulte esta excelente serie si desea obtener más información sobre Mirage.

     

    Para comenzar, navegue hasta su srcdirectorio y cree un archivo llamado server.tsejecutando los siguientes comandos:

    mkdir -p services/miragecd services/mirage# ~/diaries-app/src/services/miragetouch server.ts

    A continuación, abra el server.tsarchivo y agregue el siguiente código:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';export const handleErrors = (error: any, message = 'An error ocurred') = { return new Response(400, undefined, { data: { message, isError: true, }, });};export const setupServer = (env?: string): Server = { return new Server({ environment: env ?? 'development', models: { entry: Model.extend({ diary: belongsTo(), }), diary: Model.extend({ entry: hasMany(), user: belongsTo(), }), user: Model.extend({ diary: hasMany(), }), }, factories: { user: Factory.extend({ username: 'test', password: 'password', email: '[email protected]', }), }, seeds: (server): any = { server.create('user'); }, routes(): void { this.urlPrefix = 'https://diaries.app'; }, });};

    En este archivo, estamos exportando dos funciones. Una función de utilidad para manejar errores y setupServer()que devuelve una nueva instancia de servidor. La setupServer()función toma un argumento opcional que puede usarse para cambiar el entorno del servidor. Puede utilizar esto para configurar Mirage y realizar pruebas más adelante.

    También hemos definido tres modelos en la modelspropiedad del servidor User: Diaryy Entry. Recuerde que anteriormente configuramos la Entryinterfaz con una propiedad llamada diaryId. Este valor se establecerá automáticamente en el lugar donde idse guarda la entrada. Mirage utiliza esta propiedad para establecer una relación entre an Entryy a Diary. Lo mismo sucede también cuando un usuario crea un nuevo diario: userIdse configura automáticamente con la identificación de ese usuario.

    Sembramos la base de datos con un usuario predeterminado y configuramos Mirage para interceptar todas las solicitudes de nuestra aplicación comenzando con https://diaries.app. Observe que todavía no hemos configurado ningún controlador de ruta . Sigamos adelante y creemos algunos.

    Asegúrese de estar en el directorio src/services/mirage , luego cree un nuevo directorio llamado rutas usando el siguiente comando:

    # ~/diaries-app/src/services/miragemkdir routes

    cdal directorio recién creado y cree un archivo llamado user.ts :

    cd routestouch user.ts

    A continuación, pegue el siguiente código en el user.tsarchivo:

    import { Response, Request } from 'miragejs';import { handleErrors } from '../server';import { User } from '../../../interfaces/user.interface';import { randomBytes } from 'crypto';const generateToken = () = randomBytes(8).toString('hex');export interface AuthResponse { token: string; user: User;}const login = (schema: any, req: Request): AuthResponse | Response = { const { username, password } = JSON.parse(req.requestBody); const user = schema.users.findBy({ username }); if (!user) { return handleErrors(null, 'No user with that username exists'); } if (password !== user.password) { return handleErrors(null, 'Password is incorrect'); } const token = generateToken(); return { user: user.attrs as User, token, };};const signup = (schema: any, req: Request): AuthResponse | Response = { const data = JSON.parse(req.requestBody); const exUser = schema.users.findBy({ username: data.username }); if (exUser) { return handleErrors(null, 'A user with that username already exists.'); } const user = schema.users.create(data); const token = generateToken(); return { user: user.attrs as User, token, };};export default { login, signup,};

    Los métodos loginy signupaquí reciben una Schemaclase y un Requestobjeto falso y, al validar la contraseña o verificar que el inicio de sesión no existe, devuelven el usuario existente o un usuario nuevo respectivamente. Usamos el Schemaobjeto para interactuar con el ORM de Mirage, mientras que el Requestobjeto contiene información sobre la solicitud interceptada, incluido el cuerpo y los encabezados de la solicitud.

     

    A continuación, agreguemos métodos para trabajar con diarios y entradas de diario. Cree un archivo llamado diary.ts en su directorio de rutas :

    touch diary.ts

    Actualice el archivo con los siguientes métodos para trabajar con Diaryrecursos:

    export const create = ( schema: any, req: Request): { user: User; diary: Diary } | Response = { try { const { title, type, userId } = JSON.parse(req.requestBody) as Partial Diary ; const exUser = schema.users.findBy({ id: userId }); if (!exUser) { return handleErrors(null, 'No such user exists.'); } const now = dayjs().format(); const diary = exUser.createDiary({ title, type, createdAt: now, updatedAt: now, }); return { user: { ...exUser.attrs, }, diary: diary.attrs, }; } catch (error) { return handleErrors(error, 'Failed to create Diary.'); }};export const updateDiary = (schema: any, req: Request): Diary | Response = { try { const diary = schema.diaries.find(req.params.id); const data = JSON.parse(req.requestBody) as PartialDiary; const now = dayjs().format(); diary.update({ ...data, updatedAt: now, }); return diary.attrs as Diary; } catch (error) { return handleErrors(error, 'Failed to update Diary.'); }};export const getDiaries = (schema: any, req: Request): Diary[] | Response = { try { const user = schema.users.find(req.params.id); return user.diary as Diary[]; } catch (error) { return handleErrors(error, 'Could not get user diaries.'); }};

    A continuación, agreguemos algunos métodos para trabajar con entradas de diario: Autoclave de vapor Blog

    export const addEntry = ( schema: any, req: Request): { diary: Diary; entry: Entry } | Response = { try { const diary = schema.diaries.find(req.params.id); const { title, content } = JSON.parse(req.requestBody) as PartialEntry; const now = dayjs().format(); const entry = diary.createEntry({ title, content, createdAt: now, updatedAt: now, }); diary.update({ ...diary.attrs, updatedAt: now, }); return { diary: diary.attrs, entry: entry.attrs, }; } catch (error) { return handleErrors(error, 'Failed to save entry.'); }};export const getEntries = ( schema: any, req: Request): { entries: Entry[] } | Response = { try { const diary = schema.diaries.find(req.params.id); return diary.entry; } catch (error) { return handleErrors(error, 'Failed to get Diary entries.'); }};export const updateEntry = (schema: any, req: Request): Entry | Response = { try { const entry = schema.entries.find(req.params.id); const data = JSON.parse(req.requestBody) as PartialEntry; const now = dayjs().format(); entry.update({ ...data, updatedAt: now, }); return entry.attrs as Entry; } catch (error) { return handleErrors(error, 'Failed to update entry.'); }};

    Finalmente, agreguemos las importaciones necesarias en la parte superior del archivo:

     

    import { Response, Request } from 'miragejs';import { handleErrors } from '../server';import { Diary } from '../../../interfaces/diary.interface';import { Entry } from '../../../interfaces/entry.interface';import dayjs from 'dayjs';import { User } from '../../../interfaces/user.interface';

    En este archivo, hemos exportado métodos para trabajar con los modelos Diaryy Entry. En el createmétodo, llamamos a un método denominado user.createDiary()para guardar un nuevo diario y asociarlo a una cuenta de usuario.

    Los métodos addEntryy updateEntrycrean y asocian correctamente una nueva entrada a un diario o actualizan los datos de una entrada existente, respectivamente. Este último también actualiza la updatedAtpropiedad de la entrada con la marca de tiempo actual. El updateDiarymétodo también actualiza un diario con la marca de tiempo en que se realizó el cambio. Más adelante, ordenaremos los registros que recibamos de nuestra solicitud de red con esta propiedad.

    También tenemos un getDiariesmétodo que recupera los diarios de un usuario y getEntriesmétodos que recuperan las entradas de un diario seleccionado.

    Ahora podemos actualizar nuestro servidor para usar los métodos que acabamos de crear. Abra server.ts para incluir los archivos:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';import user from './routes/user';import * as diary from './routes/diary';

    Luego, actualizamos la propiedad del servidor routecon las rutas que queremos manejar:

    export const setupServer = (env?: string): Server = { return new Server({ // ... routes(): void { this.urlPrefix = 'https://diaries.app'; this.get('/diaries/entries/:id', diary.getEntries); this.get('/diaries/:id', diary.getDiaries); this.post('/auth/login', user.login); this.post('/auth/signup', user.signup); this.post('/diaries/', diary.create); this.post('/diaries/entry/:id', diary.addEntry); this.put('/diaries/entry/:id', diary.updateEntry); this.put('/diaries/:id', diary.updateDiary); }, });};

    Con este cambio, cuando una solicitud de red de nuestra aplicación coincide con uno de los controladores de ruta, Mirage intercepta la solicitud e invoca las funciones de controlador de ruta respectivas.

     

    A continuación, procederemos a que nuestra aplicación tenga conocimiento del servidor. Abra src/index.tsx e importe el setupServer()método:

    import { setupServer } from './services/mirage/server';

    Y agregue el siguiente código antes ReactDOM.render():

    if (process.env.NODE_ENV === 'development') { setupServer();}

    La verificación en el bloque de código anterior garantiza que nuestro servidor Mirage se ejecutará solo mientras estemos en modo de desarrollo.

    Una última cosa que debemos hacer antes de pasar a los bits de Redux es configurar una instancia de Axios personalizada para usar en nuestra aplicación. Esto ayudará a reducir la cantidad de código que tendremos que escribir más adelante.

    Cree un archivo llamado api.ts en src/services y agréguele el siguiente código:

    import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';import { showAlert } from '../util';const http: AxiosInstance = axios.create({ baseURL: 'https://diaries.app',});http.defaults.headers.post['Content-Type'] = 'application/json';http.interceptors.response.use( async (response: AxiosResponse): Promise = { if (response.status = 200 response.status 300) { return response.data; } }, (error: AxiosError) = { const { response, request }: { response?: AxiosResponse; request?: XMLHttpRequest; } = error; if (response) { if (response.status = 400 response.status 500) { showAlert(response.data?.data?.message, 'error'); return null; } } else if (request) { showAlert('Request failed. Please try again.', 'error'); return null; } return Promise.reject(error); });export default http;

    En este archivo, exportamos una instancia de Axios modificada para incluir la URL de API de nuestra aplicación, https://diaries.app . Hemos configurado un interceptor para manejar respuestas de éxito y error, y mostramos mensajes de error mediante un sweetalertbrindis que configuraremos en el siguiente paso.

    Cree un archivo nombrado util.tsen su directorio src y pegue el siguiente código en él:

    import Swal, { SweetAlertIcon } from 'sweetalert2';export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void = { Swal.fire({ titleText, position: 'top-end', timer: 3000, timerProgressBar: true, toast: true, showConfirmButton: false, showCancelButton: true, cancelButtonText: 'Dismiss', icon: alertType, showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation', }, hideClass: { popup: '', backdrop: '', }, });};

    Este archivo exporta una función que muestra un brindis cada vez que se invoca. La función acepta parámetros que le permiten configurar el mensaje y el tipo del brindis. Por ejemplo, mostramos un mensaje de error en el interceptor de errores de respuesta de Axios como este:

    showAlert(response.data?.data?.message, 'error');

    Ahora, cuando realicemos solicitudes desde nuestra aplicación mientras estamos en modo de desarrollo, Mirage las interceptará y manejará. En la siguiente sección, configuraremos nuestra tienda Redux utilizando el kit de herramientas de Redux.

     

    Configurar una tienda Redux

    En esta sección, configuraremos nuestra tienda utilizando las siguientes exportaciones del kit de herramientas de Redux configureStore(): getDefaultMiddleware()y createSlice(). Antes de comenzar, deberíamos echar un vistazo detallado a lo que hacen estas exportaciones.

    configureStore()es una abstracción de la createStore()función Redux que ayuda a simplificar su código. Se utiliza createStore()internamente para configurar su tienda con algunas herramientas de desarrollo útiles:

    export const store = configureStore({ reducer: rootReducer, // a single reducer function or an object of slice reducers});

    La createSlice()función ayuda a simplificar el proceso de creación de creadores de acciones y reductores de sectores. Acepta un estado inicial, un objeto lleno de funciones reductoras y un "nombre de segmento", y genera automáticamente creadores de acciones y tipos de acciones correspondientes a los reductores y su estado. También devuelve una única función reductora, que se puede pasar a combineReducers()la función de Redux como un "reductor de sectores".

    Recuerde que el estado es un único árbol y un único reductor de raíz gestiona los cambios en ese árbol. Para facilitar el mantenimiento, se recomienda dividir su reductor raíz en "porciones" y hacer que un "reductor de porciones" proporcione un valor inicial y calcule las actualizaciones a una porción correspondiente del estado. Estas porciones se pueden unir en una única función reductora usando combineReducers().

    Hay opciones adicionales para configurar la tienda . Por ejemplo, puede pasar una matriz de su propio middleware configureStore()o iniciar su aplicación desde un estado guardado usando la preloadedStateopción. Cuando proporciona la middlewareopción, debe definir todo el middleware que desea agregar a la tienda. Si desea conservar los valores predeterminados al configurar su tienda, puede utilizar getDefaultMiddleware()para obtener la lista predeterminada de middleware:

    export const store = configureStore({ // ... middleware: [...getDefaultMiddleware(), customMiddleware],});

    Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:

    type RootState = { auth: { token: string | null; isAuthenticated: boolean; }; diaries: Diary[]; entries: Entry[]; user: User | null; editor: { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; };}

    To get started, create a new directory named features under your src directory:

    # ~/diaries-app/srcmkdir features

    Then, cd into features and create directories named auth, diary and entry:

     

    cd featuresmkdir auth diary entry

    cd into the auth directory and create a file named authSlice.ts:

    cd auth# ~/diaries-app/src/features/authtouch authSlice.ts

    Open the file and paste the following in it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';interface AuthState { token: string | null; isAuthenticated: boolean;}const initialState: AuthState = { token: null, isAuthenticated: false,};const auth = createSlice({ name: 'auth', initialState, reducers: { saveToken(state, { payload }: PayloadAction) { if (payload) { state.token = payload; } }, clearToken(state) { state.token = null; }, setAuthState(state, { payload }: PayloadAction) { state.isAuthenticated = payload; }, },});export const { saveToken, clearToken, setAuthState } = auth.actions;export default auth.reducer;

    In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.

    Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';import { User } from '../../interfaces/user.interface';const user = createSlice({ name: 'user', initialState: null as User | null, reducers: { setUser(state, { payload }: PayloadActionUser | null) { return state = (payload != null) ? payload : null; }, },});export const { setUser } = user.actions;export default user.reducer;

    This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.

    Next, create a file named diariesSlice.ts under src/features/diary:

    # ~/diaries-app/src/featurescd diarytouch diariesSlice.ts

    Add the following code to the file:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';import { Diary } from '../../interfaces/diary.interface';const diaries = createSlice({ name: 'diaries', initialState: [] as Diary[], reducers: { addDiary(state, { payload }: PayloadActionDiary[]) { const diariesToSave = payload.filter((diary) = { return state.findIndex((item) = item.id === diary.id) === -1; }); state.push(...diariesToSave); }, updateDiary(state, { payload }: PayloadActionDiary) { const { id } = payload; const diaryIndex = state.findIndex((diary) = diary.id === id); if (diaryIndex !== -1) { state.splice(diaryIndex, 1, payload); } }, },});export const { addDiary, updateDiary } = diaries.actions;export default diaries.reducer;

    The “diaries” property of our state is an array containing the user’s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.

    Next, create a file named entriesSlice.ts under src/features/entry:

    # ~/diaries-app/src/featuresmkdir en 




    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Configuración de Redux para su uso en una aplicación del mundo real

    Configuración de Redux para su uso en una aplicación del mundo real

    Diseño de arquitectura de componentes de interfaz de usuario y tokens, con Nathan Curtis Taller de diseño conductual, con Susan y Guthrie Weinschenk Índi

    programar

    es

    https://aprendeprogramando.es/static/images/programar-configuracion-de-redux-para-su-uso-en-una-aplicacion-del-mundo-real-1051-0.jpg

    2024-05-21

     

    Configuración de Redux para su uso en una aplicación del mundo real
    Configuración de Redux para su uso en una aplicación del mundo real

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20