Reduxes una librería para manejo de estado global de una aplicación de forma predecible.- Sigue tres principios básicos:
- Una única fuente de verdad: El estado global se almacena en un único objeto JavaScript llamado store.
- El estado es de sólo lectura: El estado sólo puede cambiarse enviando acciones.
- Los cambios se realizan con funciones puras: Los reductores son funciones puras que toman el estado actual y una acción, y devuelven un nuevo estado.
- Dentro de estos conceptos Redux va a utilizar un objeto para representar el estado de nuestra aplicación y va a ser la fuente única para acceder o modificar el estado.
- Para poder cambiar el estado, un componente va a hacer un
dispatchde una acción y alguna funciónreducerva a manejar esa accción utilizando eltipo de acción. - Una
acciónno es más que un objeto de JavaScript que tiene una propiedad con el nombretypeque nos permite en las funciones reductoras (reducers) saber si el reducer tiene que hacer alguna operación para devolver un nuevo estado. - El estado va a tener un estado inicial con el que se inicia hasta que alguna acción haga que un reducer lo cambie.
- Una sección del estado se conoce como
sliceo una porción del estado. - Vamos a crear un ejemplo de contador utilizando Redux.
- El equipo de Redux recomienda utilizar
@reduxjs/toolkitpara estructurar nuestro project. - También necesitamos instalar
react-reduxya que vamos a integrarlo con React (React Native). - Ejecutamos el siguiente comando para instalar las dependencias necesarias:
npx expo install @reduxjs/toolkit react-redux@reduxjs/toolkittiene una funcióncreateSliceque nos permite crear una porción del estado que queremos manejar.createSlicees una función que acepta unestado inicial, un objeto defunciones reductoras (reducers)y unnombre de estadoy genera automáticamente unas funciones que se encargan de crearaccionesytipos de accionesque se corresponden con losreducersy elestado.- Para organizar nuestro código mejor vamos a crear una carpeta con el nombre
reduxy adentro vamos a crear otro archivo con el nombrecounterSlice.ts.
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;- En este ejemplo estamos creando un
slicecon el nombre decounterque tiene un estado inicial{value: 0}representado por un objeto con la propiedad value y el valor en 0. - Esto hace que el slice counter tenga su estado inicial en 0.
- Otra de las propiedades de este slice es
reducersque hacepta un objeto donde cada propiedad es una función que se encarga de hacer una operación y devolver un nuevo estado. - Esta librería utiliza algo llamado Immer que se encarga de modifciar el valor del estado por nosotros y devolver una nueva instancia sin que lo tengamos que hacer a mano.
- Redux mantiene el concepto de React donde el estado tiene que ser inmutable y siempre tenemos que utilizar un nuevo estado (objetos y arrays como vimos).
- Dado que estamos usando Redux Toolkit nos permite hacer todo esto de una manera más simple.
- En los reducers hay una función
incrementque incrementa el valor del estado en 1. - Otra de las funciones se llama
decrementy reduce el valor del estado (value) en 1. - Finalmente tenemos una acción que incrementa por un monto especial variable que se llama
incrementByAmount. - Todas las funciones reciben
statecomo primer parámetro que representan el estado actual. incrementByAmounttambién recibe un segundo parámetro que es la acción que se despachó. Esta acción tiene una propiedad con el nombre depayloadde donde podemos acceder al valor que cambió.- Es decir que si una función reducer no necesita un valor externo para modificar el estado lo puede hacer como hacen
increment y decrementsin necesidad de acceder a la acción. - En el caso de necesitar hacer llegar un valor al reducer para hacer alguna operación y luego devolver el nuevo estado podemos pasarle valores usando la propiedad
payloaddel action. - Al
actionlo podemos pensar como un objeto con la siguiente forma:{ type: 'incrementByAmount', payload: 2}. counterSlice.actionsretorna un objeto con todos losactionsque tiene definido el slice y los exportamos para poder utilizarlos desde otro lado.counterSlice.reducerretorna los reducers que luego vamos a utilizar para crear el store de Redux.- Ahora que sabemos como funciona
createSlice,actionsyreducerspodemos crear elstorede Redux.
@reduxjs/toolkitnos da otra función con el nombre deconfigureStoreque crea unstorede Redux que contiene el árbol de estado completo de tu aplicación.- Una de las reglas de Redux es que sólo debe haber un único almacén en tu aplicación.
- Dentro de la carpeta
reduxcreamos otro archivo con el nombrestore.tsy agregamos el siguiente código:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});- Creamos un nuevo
storeque va a almacenar el estado de nuestra app. - En este caso sólo tiene un reducer con el nombre
countery le asignamos elsliceque creamos enredux/counterSlice.ts.
- Hasta acá tenemos configurado Redux por medio de crear un slice que luego asignamos al store.
- Para que los componentes puedan acceder al estado tenemos primero que envolver la App utilizando un Provider que va a tener el
store. Es de alguna forma como cuando usamos Context. - Modificamos
app/_layout.tsxpara que utilize el Provider y así darle acceso a los componentes.
import { Stack } from "expo-router";
import { Provider } from "react-redux";
import { store } from "@/redux/store";
export default function RootLayout() {
return (
<Provider store={store}>
<Stack>
<Stack.Screen name="index" />
</Stack>
</Provider>
);
}- Como el Provider envuelve al Stack de navegación significa que en todas las pantallas podríamos acceder al estado.
- Le pasamos al Provider una propiedad con el nombre
storey le asignamos la variablestoreque creamos en la sección anterior. - De esta forma exponemos el store a todos los componentes que lo quieran consumir ya sea para leer o hacer dispatch de actions.
- Vamos a modificar el archivo
app/index.tsxpara crear un contador usando el estado.
import { Text, View, StyleSheet, Button } from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import { increment, decrement, incrementByAmount } from "@/redux/counterSlice";
export default function Index() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<View style={styles.container}>
<Text style={styles.text}>Contador: {count}</Text>
<View style={styles.row}>
<Button title="-" onPress={() => dispatch(decrement())} />
<Button title="+" onPress={() => dispatch(increment())} />
</View>
<Button
title="Incrementar en 5"
onPress={() => dispatch(incrementByAmount(5))}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
row: {
flexDirection: "row",
},
text: {
fontSize: 24,
marginBottom: 10,
},
});- Primero vamos a importar
useSelector y useDispatchdereact-redux. useSelectornos permite pasar una función que recibe como primer parámetro el estado de nuestra aplicación.- Un selector es una función que nos permite acceder al estado y retornar una sección o valor.
- En este caso vamos a utilizar el selctor para obter el valor del slice
counter.value. useDispatchnos va a retornar una funcióndispatchque nos va a permitir emitir o dispatchear acciones.- Importamos los actions creators
increment, decrement, incrementByAmountpara poder interactuar con el slice. - Al presionar alguno de los botones hacemos
dispatch(actionCreator())para emitir una acción.
console.log(decrement()); // {"payload": undefined, "type": "counter/decrement"}
console.log(increment()); // {"payload": undefined, "type": "counter/increment"}
console.log(incrementByAmount(5)); // {"payload": 5, "type": "counter/incrementByAmount"}- Al hacer console.log de los action creators vemos que no hacen más que crear un objeto que tienen una propiedad
typecon el valorcounter/actiondondecounterviene del nombre que le pusimos al slice. - Los actions tienen un payload que en algunos casos tienen el valor undefined ya que no lo estamos usando y para incrementByAmount tiene el valor de 5 ya que es el valor que le pasamos al crear el action.
- Podemos instalar un plugin para poder ver el estado utilizando una dev tool.
- Expo tiene un componente para utilizar Dev Tools que son herramientas que nos ayudan para trabajar.
- Redux tiene un plugin que se llama
redux-devtools-expo-dev-plugin. - Instalamos la herramienta:
$ npx expo install redux-devtools-expo-dev-plugin- Una vez instalado el módulo necesitamos configurarlo de la siguiente manera:
// redux/store.ts;
import { configureStore } from "@reduxjs/toolkit";
import devToolsEnhancer from "redux-devtools-expo-dev-plugin";
import counterReducer from "./counterSlice";
export type RootState = ReturnType<typeof store.getState>;
export const store = configureStore({
reducer: {
counter: counterReducer,
},
devTools: false,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(devToolsEnhancer()),
});- Por último necesitamos re iniciar el servidor de metro
npx expo starty cuando la app esté corriendo presionar en la consolashift+mpara que Expo nos muestre más herramientas de desarrollo.
- Seleccionamos la opción
Open redux-devtools-expo-dev-pluginy se debería abrir una ventana con una aplicación que nos permite ver el estado de nuestra App. - Del lado izquierdo vamos a ver las acciones que se emiten.
- Del lado derecho tenemos
Actionpara ver la acción,Statepara ver el estado,Diffpara ver como la acción cambia al estado. - Si ejecutamos algunas acciones luego podemos presionar el botón de play que está abajo a la izquierda y Redux reproduce los cambios en tiempo real y podemos viajar en el tiempo.
- Ahora vamos a agregar una lista de tareas usando redux.
- Lo primero que vamos a hacer es crear un nuevo slice llamado
redux/postSlice.ts. - Agregamos el siguiente código:
import { createSlice } from "@reduxjs/toolkit";
const todoSlice = createSlice({
name: "todos",
initialState: {
data: [], // Store list of todos
},
reducers: {
setTodos: (state, action) => {
state.data = action.payload;
},
addTodo: (state, action) => {
state.data.push({ id: Date.now(), text: action.payload });
},
updateTodo: (state, action) => {
const { id, newText } = action.payload;
const todo = state.data.find((todo) => todo.id === id);
if (todo) {
todo.text = newText;
}
},
deleteTodo: (state, action) => {
state.data = state.data.filter((todo) => todo.id !== action.payload);
},
},
});
export const { setTodos, addTodo, updateTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;- En este caso creamos el slice con el nombre
todos. - Inicializamos el estado con un objeto que tiene una propiedad con el nombre
datay un array vacio. - Agregamos un reducer y acciones para establecer (
setTodos) lostodos. - También tenemos acciones para agregar (
addTodo), actualizar (updateTodo) y borrar (deleteTodo)todos. - Exportamos por default el reducer y también todas las actions.
- Ahora tenemos que modificar el store para que use el nuevo slice.
- Modificamos el archivo
redux/store.tspara agregar el nuevo slice:
import { configureStore } from "@reduxjs/toolkit";
import devToolsEnhancer from "redux-devtools-expo-dev-plugin";
import counterReducer from "./counterSlice";
import todoReducer from "./todoSlice";
export type RootState = ReturnType<typeof store.getState>;
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer,
},
devTools: false,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(devToolsEnhancer()),
});- Agregamos el nuevo reducer con el nombre
todosy le asignamos el nuevo slice. - Ahora nos queda crear la pantalla.
- Primero vamos a hacer un pequeño refactor.
- Cambiamos el nombre de
app/index.tsxporapp/contador.tsx. - Cambiamos el nombre del componente de
IndexaContador. - Creamos un nuevo
app/index.tsxpara nuestro nuevo componente.
// app/index.tsx
import React, { useEffect, useState } from "react";
import {
View,
Text,
TextInput,
Button,
FlatList,
StyleSheet,
SafeAreaView,
Pressable,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import {
setTodos,
addTodo,
updateTodo,
deleteTodo,
Todo,
} from "@/redux/todoSlice";
import { RootState } from "@/redux/store";
import { router } from "expo-router";
const API_URL = "https://jsonplaceholder.typicode.com/todos?_limit=5";
interface TodoListitemProps {
todo: Todo;
isEditing: boolean;
onDelete: (id: number) => void;
onSetEditMode: (id: number) => void;
onSave: ({ id, text }: Todo) => void;
}
interface TodoData {
completed: boolean;
id: number;
title: string;
userId: number;
}
const TodoListItem = ({
todo,
isEditing,
onDelete,
onSetEditMode,
onSave,
}: TodoListitemProps) => {
const [editText, setEditText] = useState(todo.text);
const onDeleteHandler = () => {
onDelete(todo.id);
};
const onEditPressHandler = () => {
onSetEditMode(todo.id);
};
const onSaveHander = () => {
if (editText.trim()) {
onSave({
id: todo.id,
text: editText,
});
}
};
return (
<Pressable
style={styles.todoItem}
onPress={() =>
router.navigate({ pathname: "/todo", params: { id: todo.id } })
}
>
<View style={styles.row}>
{isEditing ? (
<TextInput
style={styles.input}
value={editText}
onChangeText={(text) => {
setEditText(text);
}}
/>
) : (
<Text style={styles.todoText}>{todo.text}</Text>
)}
</View>
<View style={styles.buttonContainer}>
{isEditing ? (
<Button title="Salvar" onPress={onSaveHander} />
) : (
<Button title="Editar" onPress={onEditPressHandler} />
)}
<Button title="Borrar" color="red" onPress={onDeleteHandler} />
</View>
</Pressable>
);
};
export default function Index() {
const dispatch = useDispatch();
const todos = useSelector((state: RootState) => state.todos.data);
const [input, setInput] = useState("");
const [editId, setEditId] = useState<number | null>(null);
useEffect(() => {
const fetchTodos = async () => {
const response = await fetch(API_URL);
const data: TodoData[] = await response.json();
const formattedTodos = data.map((item) => ({
id: item.id,
text: item.title,
}));
dispatch(setTodos(formattedTodos));
};
fetchTodos();
}, []);
const handleAddTodo = () => {
if (input.trim()) {
dispatch(addTodo(input));
setInput("");
}
};
const handleUpdateTodo = ({ id, text }: Todo) => {
dispatch(updateTodo({ id, newText: text }));
setEditId(null);
};
const handleDeleteTodo = (id: number) => {
dispatch(deleteTodo(id));
};
const handleSetEditMode = (id: number) => {
setEditId(id);
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Ingrese un nuevo Todo"
value={input}
onChangeText={setInput}
/>
<Button title="Agregar" onPress={handleAddTodo} />
</View>
<FlatList
data={todos}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TodoListItem
todo={item}
isEditing={editId === item.id}
onDelete={handleDeleteTodo}
onSetEditMode={handleSetEditMode}
onSave={handleUpdateTodo}
/>
)}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
padding: 20,
},
inputContainer: {
flexDirection: "row",
marginVertical: 10,
alignItems: "center",
},
input: {
flex: 1,
borderBottomWidth: 1,
padding: 8,
marginRight: 10,
},
todoItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#fff",
marginVertical: 5,
borderRadius: 5,
elevation: 3,
},
todoText: {
fontSize: 14,
},
buttonContainer: {
flexDirection: "row",
justifyContent: "flex-end",
gap: 5,
width: "40%",
},
row: {
paddingLeft: 10,
width: "60%",
},
});- En este componente pasa de todo!! buscá 🍿 y 🥤.
- Inicialmente importamos todo lo que necesitamos. Varios componentes de
react-native,useSelectoryuseDispatchdereact-reduxpara poder interactuar con el estado, las acciones que definimos en el slice@/redux/todoSlicey finalmenterouterdeexpo-routerpara poder navegar a otra pantalla desde este componente. - En esta pantalla tenemos definido 2 componentes. Por un lado el contenido de la pantalla en sí, más un componente
TodoListItemque es lo que vamos a renderizar para cadaTodo. - Por ahora nos enfocamos en el componente
Indexpara ver el contenido de la pantalla.
useEffect(() => {
const fetchTodos = async () => {
const response = await fetch(API_URL);
const data: TodoData[] = await response.json();
const formattedTodos = data.map((item) => ({
id: item.id,
text: item.title,
}));
dispatch(setTodos(formattedTodos));
};
fetchTodos();
}, []);- Usamos
useEffectcon dependencia vacía para que se ejecute una vez. - Inicialmente cargamos unos Todo's de un sitio remoto que está definido en la variable
API_URL. - Una vez obtenidos los valores que queremos los seteamos en el estado que creamos con Redux usando la función
dispatchpara emitir una acciónsetTodosusando los Todos que obtuvimos haciendofetch. - El estado inicial lo definimos como
[]con un array vacío. - Cuando se ejecuta esta acción el reducer de
reduxtoma los datos y los asigna al estado. - Una vez ejecutado esto la variables
todostiene una colección detodos.
const dispatch = useDispatch();
const todos = useSelector((state: RootState) => state.todos.data);
const [input, setInput] = useState("");
const [editId, setEditId] = useState<number | null>(null);- Después de definir el componente Index usamos
useDispatchpara acceder a la funcióndispatch. useSelectornos permite acceder al estado que creamos en el slice de los todos.state.todos.datatiene inicialmente el array vacío que definimos como estado inicial y luego de cargar los datos tiene la colección de Todo's que obtuvimos de manera remota.- Luego utilizamos 2 veces
useStatepara crear variables de estado desoloeste componente. - Es decir que tenemos un
estado globalpara nuestra appusando reduxy unestado localde los componentes que necesitan tener estadousando useState. - Ahora nos enfocamos en lo que estamos renderizando:
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Ingrese un nuevo Todo"
value={input}
onChangeText={setInput}
/>
<Button title="Agregar" onPress={handleAddTodo} />
</View>
<FlatList
data={todos}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TodoListItem
todo={item}
isEditing={editId === item.id}
onDelete={handleDeleteTodo}
onSetEditMode={handleSetEditMode}
onSave={handleUpdateTodo}
/>
)}
/>
</View>
</SafeAreaView>
);
}- SafeAreaView para mantener el contenido en un espacio de la pantalla visible.
- Tenemos una especie de encabezado que tiene un TextInput para ingresar un texto. Al cambiar el texto se ejecuta setInput para establecer el estado de ese texto.
- Como valor del input usamos la variable
inputque creamos en el estado. De esta forma usandosetInputeinputcontrolamos el valor que el usuario ingresa en el campo de texto. La funciónonChangeTextse encarga de cambiar el estado cada vez que el usuario ingresa o modifica el texto. - Luego tenemos un
Buttoncon el textoAgregarque al ser presionado llama a la funciónhandleAddTodo. - En este momento tenemos que agregar un nuevo Todo a la lista.
const handleAddTodo = () => {
if (input.trim()) {
dispatch(addTodo(input));
setInput("");
}
};handleAddTodoverifica que el texto no esté vacío usandoinput.trim.- Si hay un texto entonces hace dispatch del action
addTodopasando el texto de este todo. - Luego limpiamos el valor del estado usando
setInputasí el campo de texto se ve vacío. - Ahora analicemos la lista de todos:
<FlatList
data={todos}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TodoListItem
todo={item}
isEditing={editId === item.id}
onDelete={handleDeleteTodo}
onSetEditMode={handleSetEditMode}
onSave={handleUpdateTodo}
/>
)}
/>- Usamos un FlatList para mostrar los todos.
- Le pasamos
todosa la propiedad data para que la lista sepa cual es la colección con la que estamos trabajando y que valores tiene que usar para cada item de la lista. - Usamos
keyExtractorpara decirle a React como poder saber cuando cambia un item. - Finalmente usamos
renderItempara renderizar cada item de la colección que para nosotros es un todo. - Para no tener un código muy largo en la lista podemos usar un componente para renderizar cada todo.
<TodoListItem
todo={item}
isEditing={editId === item.id}
onDelete={handleDeleteTodo}
onSetEditMode={handleSetEditMode}
onSave={handleUpdateTodo}
/>-
TodoListItemtiene varias propiedades:todo={item}: Le pasamos la propiedad todo que es cada item de la colección de la lista que viene del estado todos.isEditing={editId === item.id}: Este componente necesita saber si está en modo edit o solo mostrando el contenido. Si la variable editId tiene seteado el valor deitem.idesta comparación se transforma entruepor ende seteando el componente en modo edición en lugar de sólo mostrar el valor del todo.onDelete={handleDeleteTodo}: este es un callback que el componente va a llamar a cuando el usuario presione el botón de borrar el todo. Dado que queremos que el estado lo maneje la pantalla y no el componente tenemos que manejar la interación con este componente.onSetEditMode={handleSetEditMode}: al presionarEditarnecesitamos poner el componente en modo edit. Para eso seteamos el valor de estado editId y con eso logramos que al re-renderizar la propiedadisEditingse tranforme en true.onSave={handleUpdateTodo}: maneja cuando el usuario presiona el botón Salvar.
-
Ahora que sabemos todas las propiedades que usa el componente
TodoListItempodemos ver como se renderiza:
const TodoListItem = ({
todo,
isEditing,
onDelete,
onSetEditMode,
onSave,
}: TodoListitemProps) => {
const [editText, setEditText] = useState(todo.text);
const onDeleteHandler = () => {
onDelete(todo.id);
};
const onEditPressHandler = () => {
onSetEditMode(todo.id);
};
const onSaveHander = () => {
if (editText.trim()) {
onSave({
id: todo.id,
text: editText,
});
}
};
return (
<Pressable
style={styles.todoItem}
onPress={() =>
router.navigate({ pathname: "/todo", params: { id: todo.id } })
}
>
<View style={styles.row}>
{isEditing ? (
<TextInput
style={styles.input}
value={editText}
onChangeText={(text) => {
setEditText(text);
}}
/>
) : (
<Text style={styles.todoText}>{todo.text}</Text>
)}
</View>
<View style={styles.buttonContainer}>
{isEditing ? (
<Button title="Salvar" onPress={onSaveHander} />
) : (
<Button title="Editar" onPress={onEditPressHandler} />
)}
<Button title="Borrar" color="red" onPress={onDeleteHandler} />
</View>
</Pressable>
);
};- Este componente necesita tener su estado propio mientras está en modo edición. Para eso creamos un estado usando
const [editText, setEditText] = useState(todo.text).
const onDeleteHandler = () => {
onDelete(todo.id);
};
const onEditPressHandler = () => {
onSetEditMode(todo.id);
};
const onSaveHander = () => {
if (editText.trim()) {
onSave({
id: todo.id,
text: editText,
});
}
};- Definimos varias funciones que van a manjar la interación entre los eventos del componente y lo que tiene que hacer con las propiedades que le pasaron como parámetro.
onDeleteHandlerllama aonDeletepasandole el id del todo que queremos borrar. Después la pantalla se ocupa de manejar el estado general y que se re-renderize la lista.onEditPressHandler: establece cual es eliddel todo que queremos editar. Esto modifica el estado y finalmente nuestro componente recibe la propiedadisEditingen true. Con esto vamos a poder validar / mostrar el modo edición.onSaveHander: es el que se encarga de decirle a la pantalla que el todo que fué editado está listo para salvar los datos. Usa el id del todo original y como texto pasamos el texto editado que está en el componente. Al llamar aonSavela pantalla va a actualizar el estado de redux usando el nuevo texto.- Cada item usa un componente
Pressablepara navegar de manera dinámica a otra pantalla que ya vamos a crear con el nombre detodoque es un detalle del todo. - Para saber que todo tiene que mostrar en la pantalla
todo.tsxle pasamos un parámetro con el valor del id del todo presionado:
<Pressable
style={styles.todoItem}
onPress={() =>
router.navigate({ pathname: "/todo", params: { id: todo.id } })
}
>- usamos
router.navigatepara navegar a la otra pantalla usandotodocomo pathname y params con un objecto con el id del id del todo.
<View style={styles.row}>
{isEditing ? (
<TextInput
style={styles.input}
value={editText}
onChangeText={(text) => {
setEditText(text);
}}
/>
) : (
<Text style={styles.todoText}>{todo.text}</Text>
)}
</View>- Esta sección maneja si muestra un campo de edición del todo o sólo el texto.
- Si el modo
isEditinges verdadero entonces muestra el campo de texto. - Si el modo
isEditinges false entonces muestra el texto del todo original.
- Si el modo
- En caso de estar en modo edición entonces tenemos que mostrar el valor de la variable de estado que va a manejar los cambios. Por eso pasamos
editTextcomo valor inicial. Esta variable al renderizar el componente siempre usa el valor del texto del todo como valor inicial. - Usamos
onChangeTextdel input para poder saber cuando el usuario cambia un valor en el campo de texto. En ese caso modificamos el valor de la variable del estado y de esa forma conseguimos mostrar el texto editado en lugar del texto original. - Ahora que sabemos esto necesitamos saber como se cambia de estado a edit y como se salva!.
<View style={styles.buttonContainer}>
{isEditing ? (
<Button title="Salvar" onPress={onSaveHander} />
) : (
<Button title="Editar" onPress={onEditPressHandler} />
)}
<Button title="Borrar" color="red" onPress={onDeleteHandler} />
</View>- Esta parte del componente renderiza botones.
- Los botones cambian según si el componente está en modo edit o no.
- Siempre muestra un botón de
Borrarque al presionarlo llama a la función onDeleteHandler. - En caso de estar editando mostramos un botón con el mensaje
salvary llamamos a la funciónonSaveHandler. - Si queremos establecer el modo edit entonces usamos el botón que dice
Editary que llama aonEditPressHandler. - Lo que falta ver es que hace la pantalla con todos estos callbacks.
const handleAddTodo = () => {
if (input.trim()) {
dispatch(addTodo(input));
setInput("");
}
};
const handleUpdateTodo = ({ id, text }: Todo) => {
dispatch(updateTodo({ id, newText: text }));
setEditId(null);
};
const handleDeleteTodo = (id: number) => {
dispatch(deleteTodo(id));
};
const handleSetEditMode = (id: number) => {
setEditId(id);
};- Como ya vimos estas funciones están haciendo dispatch de los diferentes actions que maneja el slice de los todos.
- Al crear un todo hacemos dispatch de la acción
addTodoy le pasamos el valor de un campo de texto. - Al hacer update de un todo le tenemos que pasar el
iddel todo que queremos actualizar y el nuevo texto haciendo dispatch del actionupdateTodo. - Para borrar un todo necesitamos saber el
idy hacer dispatch de la accióndeleteTodo. - Finalmente si estamos editando un todo en este caso no necesitamos modificar el estado global sino el estado local del componente.
- Ahora es un buen momento de ir a ver / analizar que hacen los reducers cuando se llaman a estas actions, abramos el archivo del slice de todos.
setTodos: (state, action) => {
state.data = action.payload;
},- Cuando se ejecuta un action
setTodosse asigna el valor pasado comopayloadal estado que tiene la variabledata. - Como esto usa un módulo llamado Immer no tenemos que preocuparmos de manejar que los array sean instancias nuevas y todo eso que aprendimos porque para eso tenemos a este modulo que nos simplifica nuestro trabajo.
addTodo: (state, action) => {
state.data.push({ id: Date.now(), text: action.payload });
},- addTodo action le asigna un
idrandom y un text usando el texto que pasamos como payload. - Luego toma ese nuevo valor y lo agrega a la colección del estado.
- Internamente Immer se encarga de crear un nuevo array y todo lo que ya sabemos.
updateTodo: (state, action) => {
const { id, newText } = action.payload;
const todo = state.data.find((todo) => todo.id === id);
if (todo) {
todo.text = newText;
}
},- Actualizar un todo es un poco más complicado.
- Primero tomamos los datos del payload para saber el
iddel todo que necesitamos modificar y elnewTextque es el valor del nuevo texto. - Luego buscamos el todo en la colección usando
find. - Una vez encontrado el todo podemos usar la propiedad
textpara modificar ese todo. - De nuevo, gracias al módulo que usa redux no tenemos que ocuparnos de re-crear todos los items del array, hacer spread y todo eso y nos enfocamos sólo en lo que queremos cambiar.
deleteTodo: (state, action) => {
state.data = state.data.filter((todo) => todo.id !== action.payload);
},- Por último usamos filter para buscar el todo que queremos borrar de la lista y creamos un nuevo estado sin ese elemento.
- Podemos ver todas estas cosas en el devtools para ver como se emiten las acciones, que valores se pasan como parámetro y también como se actualiza el estado.
- Ahora terminemos la app. Primero a definir la pantalla
app/todo.tsx:
import { View, Text, StyleSheet } from "react-native";
import { useLocalSearchParams } from "expo-router";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
const Todo = () => {
const todos = useSelector((state: RootState) => state.todos.data);
const { id } = useLocalSearchParams();
const todo = todos.find((todoItem) => parseInt(id) === todoItem.id);
return (
<View style={styles.container}>
{todo && <Text style={styles.text}>ID: {todo.id}</Text>}
{todo && <Text style={styles.text}>Text: {todo.text}</Text>}
</View>
);
};
export default Todo;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
text: {
fontSize: 24,
},
});- Esta pantalla recibe el parámetro id y usa un selector como para encontrar el todo en el estado.
- También podemos modificar
app/_layout.tsxpara manejar las rutas:
import { Link, Stack } from "expo-router";
import { Provider } from "react-redux";
import { store } from "@/redux/store";
export default function RootLayout() {
return (
<Provider store={store}>
<Stack>
<Stack.Screen
name="index"
options={{
title: "Todos",
headerRight: () => <Link href="/contador">Ir a Contador</Link>,
}}
/>
<Stack.Screen name="contador" options={{ title: "Contador" }} />
<Stack.Screen name="todo" options={{ title: "Todo Detail" }} />
</Stack>
</Provider>
);
}- En este caso le asignamos un title a cada ruta para que sea más descriptivo.
- También le agregamos un encabezado derecho a la pantalla index para poder navegar al contador.
- Con esto cerramos nuestra app de redux!
- Podes aprender más sobre Redux leyendo la documentación oficial.
- Otro logro más, felicitaciones! ahora aprendiste las bases de Redux.

