Skip to content

Adding a feature

This walkthrough adds a Todos feature: an API hook, a screen with a list, a route, translations, and a persisted filter. The same steps work for any feature.

  1. Create the folder

    src/features/todos/
    ├── api/
    │ ├── types.ts
    │ └── use-todos.ts
    ├── screens/
    │ └── TodosScreen.tsx
    └── index.ts
  2. Add the API hook

    src/features/todos/api/types.ts
    export type Todo = { id: number; todo: string; completed: boolean };
    export type TodosPage = { todos: Todo[]; total: number; skip: number; limit: number };
    src/features/todos/api/use-todos.ts
    import { createQuery } from 'react-query-kit';
    import type { AxiosError } from 'axios';
    import { client } from '@/services';
    import type { TodosPage } from './types';
    export const useTodos = createQuery<TodosPage, void, AxiosError>({
    queryKey: ['todos'],
    fetcher: (_, { signal }) => client.get('todos', { signal }).then((r) => r.data),
    });

    DummyJSON has a todos endpoint, so this works against the development API as is.

  3. Add translations

    Add a todos section to en.json, es.json and ar.json:

    src/localization/translations/en.json
    "todos": {
    "title": "Todos",
    "empty": "Nothing to do"
    }
  4. Build the screen

    src/features/todos/screens/TodosScreen.tsx
    import { FlatList, Text } from 'react-native';
    import { useTranslate } from '@/localization/utils';
    import ScreenWrapper from '@/shared/components/ScreenWrapper';
    import { useScreenLoader } from '@/shared/hooks';
    import { TextStyles } from '@/theme';
    import { useTodos } from '../api/use-todos';
    export function TodosScreen() {
    const t = useTranslate();
    const { data, isLoading } = useTodos();
    useScreenLoader(isLoading);
    return (
    <ScreenWrapper>
    <Text style={TextStyles.h1}>{t('todos.title')}</Text>
    <FlatList
    data={data?.todos ?? []}
    keyExtractor={(item) => String(item.id)}
    renderItem={({ item }) => <Text>{item.todo}</Text>}
    ListEmptyComponent={<Text>{t('todos.empty')}</Text>}
    />
    </ScreenWrapper>
    );
    }

    Inside the feature, imports are relative (../api/use-todos).

  5. Export the public API

    src/features/todos/index.ts
    export { TodosScreen } from './screens/TodosScreen';
    export { useTodos } from './api/use-todos';
    export type { Todo } from './api/types';
  6. Add a route

    src/app/todos.tsx
    export { TodosScreen as default } from '@/features/todos';

    That’s the /todos route. Navigate with router.push('/todos') or <Link href="/todos">.

    To make it a tab instead, put the file in src/app/(tabs)/, add <Tabs.Screen name="todos" /> to src/app/(tabs)/_layout.tsx, and register its icon and label in TabBarIcon.tsx and TabBarLabel.tsx.

  7. Add state (if you need it)

    For app state that should survive restarts, like a “hide completed” toggle, add a store inside the feature and expose a hook from index.ts. See State management. Remember to register Zustand stores in rehydrateStores().

  8. Test it

    Terminal window
    yarn lint && yarn type-check && yarn test
  • Everything lives in src/features/<name>/
  • Other code imports it only through index.ts
  • API calls use client from @/services and pass signal
  • Every string is in all three translation files
  • New Zustand stores are added to rehydrateStores()