civil-and-structural-engineering
Building a Task Management App with React Native and Redux
Table of Contents
Introduction
Building a task management application remains one of the most effective ways to master mobile development with React Native. The combination of React Native for cross-platform UI and Redux for predictable state management creates a robust foundation that scales from simple to‑do lists to complex project management tools. This expanded guide walks through every layer of constructing a production‑ready task app, including state normalization, middleware, navigation, persistence, and testing. By the end you will have a complete, extensible application and a deeper understanding of how to structure real‑world mobile projects.
Setting Up the Project
Initializing the React Native project
Start by creating a new React Native project with the latest CLI. Use the command:
npx react-native@latest init TaskManager
This gives you a standard project structure with android/, ios/, and App.tsx. If you prefer TypeScript (recommended), the CLI will prompt you to choose a template – select the TypeScript variant to get strong typing from the start.
Installing core dependencies
Once the project is created, install Redux, React‑Redux, and the Redux Toolkit (the modern, recommended way to write Redux logic):
npm install @reduxjs/toolkit react-redux
For navigation we will use React Navigation. Install the required packages:
npm install @react-navigation/native @react-navigation/stack
npx pod-install ios
If you plan to persist tasks locally, also install @react-native-async-storage/async-storage. We will cover persistence later.
Designing the State Structure
A well‑designed Redux state is the backbone of any scalable app. For a task manager, the state should not only hold a flat list of tasks but also allow for features like filtering, sorting, and undo. The following schema balances simplicity with future extensibility:
{
tasks: {
byId: {
"1": { id: "1", title: "Buy groceries", description: "", completed: false, createdAt: 1623456789, listId: "inbox" },
"2": { id: "2", title: "Read a book", description: "Finish chapter 5", completed: true, createdAt: 1623456790, listId: "personal" }
},
allIds: ["1", "2"],
filter: "ALL" // "ALL", "ACTIVE", "COMPLETED"
},
lists: {
byId: {
"inbox": { id: "inbox", name: "Inbox", icon: "inbox" },
"personal": { id: "personal", name: "Personal", icon: "person" }
},
allIds: ["inbox", "personal"]
},
ui: {
isAddingTask: false,
selectedTaskId: null
}
}
By normalizing the data (instead of an array of tasks), updating or deleting a task becomes an O(1) operation. The allIds arrays preserve ordering. Separate slices for tasks, lists, and UI concerns keep the store predictable.
Implementing Redux with Redux Toolkit
Creating slices
Redux Toolkit simplifies action and reducer creation with createSlice. Below is the tasks slice:
// features/tasks/tasksSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: number;
listId: string;
}
interface TasksState {
byId: Record;
allIds: string[];
filter: 'ALL' | 'ACTIVE' | 'COMPLETED';
}
const initialState: TasksState = {
byId: {},
allIds: [],
filter: 'ALL',
};
const tasksSlice = createSlice({
name: 'tasks',
initialState,
reducers: {
addTask(state, action: PayloadAction<Omit<Task, 'createdAt'>>) {
const task: Task = { ...action.payload, createdAt: Date.now() };
state.byId[task.id] = task;
state.allIds.push(task.id);
},
toggleTask(state, action: PayloadAction<string>) {
const task = state.byId[action.payload];
if (task) task.completed = !task.completed;
},
deleteTask(state, action: PayloadAction<string>) {
delete state.byId[action.payload];
state.allIds = state.allIds.filter(id => id !== action.payload);
},
setFilter(state, action: PayloadAction<TasksState['filter']>) {
state.filter = action.payload;
},
},
});
export const { addTask, toggleTask, deleteTask, setFilter } = tasksSlice.actions;
export default tasksSlice.reducer;
Notice that Redux Toolkit allows writing “mutating” logic in reducers (thanks to Immer under the hood) – it is safe and much cleaner.
Configuring the store
Create the store by combining slices. For now we only have tasks, but you can add lists and UI slices later.
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import tasksReducer from '../features/tasks/tasksSlice';
export const store = configureStore({
reducer: {
tasks: tasksReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Adding middleware (optional)
If you need to perform side effects (e.g., saving to AsyncStorage), Redux Toolkit already includes redux-thunk by default. You can create thunks to handle async logic:
// features/tasks/tasksThunks.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { RootState } from '../../app/store';
export const persistTasks = createAsyncThunk(
'tasks/persist',
async (_, { getState }) => {
const state = getState() as RootState;
await AsyncStorage.setItem('tasks', JSON.stringify(state.tasks));
}
);
Building the User Interface
Navigation structure
Use a stack navigator for the main flow: a list screen and a detail screen. Create two screens:
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import TaskListScreen from './screens/TaskListScreen';
import TaskDetailScreen from './screens/TaskDetailScreen';
const Stack = createStackNavigator();
export default function App() {
return (
<Provider store={store}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="TaskList" component={TaskListScreen} options={{ title: 'My Tasks' }} />
<Stack.Screen name="TaskDetail" component={TaskDetailScreen} />
</Stack.Navigator>
</NavigationContainer>
</Provider>
);
}
Task list component
The list screen uses a FlatList and connects to Redux with hooks (useSelector and useDispatch):
// screens/TaskListScreen.tsx
import React, { useState } from 'react';
import { View, FlatList, TextInput, Button, Text } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../app/store';
import { addTask, toggleTask, deleteTask } from '../features/tasks/tasksSlice';
export default function TaskListScreen() {
const [newTaskTitle, setNewTaskTitle] = useState('');
const dispatch = useDispatch();
const tasks = useSelector((state: RootState) => state.tasks);
const visibleTasks = tasks.allIds
.map(id => tasks.byId[id])
.filter(task => {
if (tasks.filter === 'ACTIVE') return !task.completed;
if (tasks.filter === 'COMPLETED') return task.completed;
return true;
});
const handleAdd = () => {
if (newTaskTitle.trim()) {
dispatch(addTask({ id: Date.now().toString(), title: newTaskTitle, description: '', completed: false, listId: 'inbox' }));
setNewTaskTitle('');
}
};
const renderItem = ({ item }) => (
<View style={{ flexDirection: 'row', alignItems: 'center', padding: 12 }}>
<Text onPress={() => dispatch(toggleTask(item.id))} style={{ flex: 1, textDecorationLine: item.completed ? 'line-through' : 'none' }}>{item.title}</Text>
<Button title="Delete" onPress={() => dispatch(deleteTask(item.id))} color="red" />
</View>
);
return (
<View>
<View style={{ flexDirection: 'row', padding: 8 }}>
<TextInput
placeholder="New task"
value={newTaskTitle}
onChangeText={setNewTaskTitle}
style={{ flex: 1, borderBottomWidth: 1, marginRight: 8 }}
/>
<Button title="Add" onPress={handleAdd} />
</View>
<FlatList data={visibleTasks} keyExtractor={item => item.id} renderItem={renderItem} />
</View>
);
}
Task detail screen
For displaying and editing a single task, create a detail screen that retrieves the task from the store by route params:
// screens/TaskDetailScreen.tsx
import React from 'react';
import { View, Text, TextInput, Switch } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../app/store';
import { toggleTask } from '../features/tasks/tasksSlice';
export default function TaskDetailScreen({ route, navigation }) {
const { taskId } = route.params;
const task = useSelector((state: RootState) => state.tasks.byId[taskId]);
const dispatch = useDispatch();
if (!task) return <Text>Task not found</Text>;
return (
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 24 }}>{task.title}</Text>
<Text>Created: {new Date(task.createdAt).toLocaleString()}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Text>Completed: </Text>
<Switch value={task.completed} onValueChange={() => dispatch(toggleTask(task.id))} />
</View>
</View>
);
}
Filtering tasks
Add a filter bar at the top of the list screen using setFilter:
// inside TaskListScreen
import { setFilter } from '../features/tasks/tasksSlice';
// UI for filter buttons
<View style={{ flexDirection: 'row', justifyContent: 'space-around', padding: 8 }}>
<Button title="All" onPress={() => dispatch(setFilter('ALL'))} />
<Button title="Active" onPress={() => dispatch(setFilter('ACTIVE'))} />
<Button title="Completed" onPress={() => dispatch(setFilter('COMPLETED'))} />
</View>
Connecting Components to Redux
In modern React Native apps the recommended way to connect components is via React‑Redux hooks (useSelector and useDispatch) as shown above. For class components you can still use the connect HOC, but hooks simplify code and avoid the complexity of mapped props.
When using useSelector, every time the selected state changes the component re‑renders. To prevent unnecessary re‑renders (e.g., when only a part of the state changes), you can use the shallowEqual comparison:
import { shallowEqual, useSelector } from 'react-redux';
const taskIds = useSelector(state => state.tasks.allIds, shallowEqual);
Adding Features
Task persistence with AsyncStorage
To avoid losing tasks on app restart, persist the Redux state. Use a custom middleware or subscribe to store changes. A simple approach:
// In store.ts after creation
import AsyncStorage from '@react-native-async-storage/async-storage';
store.subscribe(() => {
const state = store.getState();
AsyncStorage.setItem('tasks', JSON.stringify(state.tasks));
});
// On app start, load the persisted state and dispatch an action to hydrate
const loadPersistedState = async () => {
const storedTasks = await AsyncStorage.getItem('tasks');
if (storedTasks) {
store.dispatch({ type: 'tasks/hydrate', payload: JSON.parse(storedTasks) });
}
};
loadPersistedState();
For a more robust solution, consider using Redux Toolkit’s listener middleware or the redux-persist library.
Task lists and organization
Allow users to create multiple lists (e.g., Work, Personal, Shopping). Add a lists slice and an addList action. Modify the task list screen to show tasks filtered by a selected list. Use React Navigation’s bottom tab navigator or a simple picker component.
Undo / Redo
Redux makes undo straightforward by keeping a history of state snapshots. You can create a custom reducer that wraps other reducers and stores an array of previous states. Alternatively, use the redux-undo library which integrates seamlessly with existing slices.
Testing
Unit testing reducers and selectors
Reducers are pure functions – easy to test with Jest. Create a tasksSlice.test.ts:
import tasksReducer, { addTask, toggleTask } from './tasksSlice';
describe('tasks reducer', () => {
it('should add a task', () => {
const initialState = { byId: {}, allIds: [], filter: 'ALL' };
const nextState = tasksReducer(initialState, addTask({ id: '1', title: 'Test', description: '', completed: false, listId: 'inbox' }));
expect(nextState.allIds).toContain('1');
expect(nextState.byId['1'].title).toBe('Test');
});
it('should toggle a task', () => {
const state = { byId: { '1': { id: '1', title: 'Test', completed: false, createdAt: 0, description: '', listId: 'inbox' } }, allIds: ['1'], filter: 'ALL' };
const nextState = tasksReducer(state, toggleTask('1'));
expect(nextState.byId['1'].completed).toBe(true);
});
});
Component testing
For integration tests, use React Native Testing Library together with a mock Redux store. Write tests that render a component wrapped in a Provider, then dispatch actions and assert that the UI updates correctly.
import { render, fireEvent } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import tasksReducer from '../features/tasks/tasksSlice';
import TaskListScreen from './TaskListScreen';
const createTestStore = (preloadedState) => configureStore({
reducer: { tasks: tasksReducer },
preloadedState,
});
test('displays tasks and allows toggling', () => {
const store = createTestStore({
tasks: {
byId: { '1': { id: '1', title: 'Test', completed: false, createdAt: 0, description: '', listId: 'inbox' } },
allIds: ['1'],
filter: 'ALL',
},
});
const { getByText, getByDisplayValue } = render(
<Provider store={store}>
<TaskListScreen />
</Provider>
);
expect(getByText('Test')).toBeTruthy();
});
Deployment and Performance
Before deploying to the app stores, run a production build to ensure performance is optimised:
npx react-native run-android --variant=release
npx react-native run-ios --configuration Release
For Android, generate a signed APK by following the official React Native documentation. For iOS, archive the app in Xcode and upload to App Store Connect.
Performance considerations: use useMemo and useCallback for list item components, consider implementing React.memo on task rows, and use FlatList’s getItemLayout for fixed‑height items to enable faster scrolling.
Conclusion
Building a task management app with React Native and Redux teaches you the complete mobile development cycle – from project setup and state architecture to UI implementation, testing, and deployment. The patterns shown here (normalized state, toolkit slices, navigation, persistence) are directly applicable to larger, more complex applications. To deepen your knowledge, explore the official Redux documentation and the React Native docs. Experiment with advanced features like offline support, push notifications, or real‑time sync with a backend. This foundation will serve you well for any React Native project.