Share
1

Data Fetching and Redux Integration in React Applications

by ObserverPoint · July 16, 2025

As React applications grow in size and complexity, simply throwing components into a single folder or mixing all concerns within one component quickly leads to a tangled and unmanageable codebase. Establishing clear architectural patterns is crucial for maintaining readability, reusability, testability, and scalability. This article will delve into three fundamental concepts that help structure large React applications: the Container/Presentational components pattern, Atomic Design principles, and effective folder structures.

Adopting these patterns, especially when combined with state management solutions like Redux Toolkit and type safety from TypeScript, provides a robust framework for building enterprise-grade applications that are easy to develop, debug, and evolve over time.


Container/Presentational Components Pattern

The Container/Presentational (or Smart/Dumb) components pattern, popularized by Dan Abramov, advocates for separating concerns within your React component tree. It divides components into two main categories:

1. Presentational (or “Dumb”) Components

  • Concern: How things look.
  • Characteristics:
    • Receive data and callbacks via props.
    • Rarely have their own state (unless it’s UI state like a toggle).
    • Focus on rendering UI elements based on props.
    • Are typically stateless functional components (or functional components using `useState` for local UI state).
    • Highly reusable and easily testable.
  • Example: A `Button`, `UserAvatar`, `ProductCard`, `List` component. They don’t know where the data comes from or how actions are handled; they just render what they’re told.
<!-- src/components/Button/Button.tsx -->
import React from 'react';
import './Button.css'; // Optional styling

interface ButtonProps {
    onClick: () => void;
    children: React.ReactNode;
    isDisabled?: boolean;
}

// Presentational component: focuses on rendering
const Button: React.FC<ButtonProps> = ({ onClick, children, isDisabled = false }) => {
    return (
        <button onClick={onClick} disabled={isDisabled} className="my-button">
            {children}
        </button>
    );
};

export default Button;
    

2. Container (or “Smart”) Components

  • Concern: How things work.
  • Characteristics:
    • Fetch data (e.g., from Redux store, API calls).
    • Contain business logic and state management.
    • Pass data and callbacks to presentational child components via props.
    • Are often connected to the Redux store (using `useSelector` and `useDispatch`).
    • Less reusable (specific to application logic), but easier to manage due to focused responsibility.
  • Example: A `UserListContainer` (fetches users, passes to `UserList`), `ProductPage` (fetches product data, handles adding to cart).
<!-- src/containers/CounterContainer.tsx -->
import React from 'react';
import { useAppSelector, useAppDispatch } from '../app/hooks'; // Your typed Redux hooks
import { increment, decrement } from '../features/counter/counterSlice'; // Redux actions
import Button from '../components/Button/Button'; // Presentational Button component

// Container component: manages state and logic, passes to presentational components
const CounterContainer: React.FC = () => {
    const count = useAppSelector((state) => state.counter.value);
    const dispatch = useAppDispatch();

    const handleIncrement = () => {
        dispatch(increment());
    };

    const handleDecrement = () => {
        dispatch(decrement());
    };

    return (
        <div>
            <h2>Counter Value: {count}</h2>
            <Button onClick={handleIncrement}>Increment</Button>
            <Button onClick={handleDecrement}>Decrement</Button>
        </div>
    );
};

export default CounterContainer;
    

Benefits: Clear separation of concerns, improved reusability of presentational components, easier testing (presentational components can be tested in isolation with mock props, container components can be tested for logic). While strict adherence can sometimes lead to an extra layer of components, it’s a valuable mental model for structuring your application.


Atomic Design Principles

Atomic Design, created by Brad Frost, is a methodology for crafting design systems. It breaks down UI into smaller, more manageable pieces and then combines them to create larger, more complex ones. This approach maps very well to React’s component-based architecture:

  1. Atoms: The smallest fundamental building blocks of matter. In UI, these are single HTML elements or basic React components that cannot be broken down further without losing their meaning. They serve as the foundational elements of our interfaces.
    • Examples: Labels, inputs, buttons, icons, headings, paragraphs.
    • React Mapping: Smallest, most reusable presentational components.
  2. Molecules: Groups of atoms bonded together to form a relatively simple, functional unit. They take on properties of their own, becoming distinct components.
    • Examples: A search form (input + button), a navigation item (icon + text link), an alert message (text + close button).
  3. Organisms: Relatively complex UI components composed of groups of molecules and/or atoms that form a distinct section of an interface. They are often self-contained and can be reused on different pages.
    • Examples: A header (logo, navigation, search bar), a product grid (multiple product cards), a user profile block.
  4. Templates: Page-level objects that place organisms into a layout. They focus on the underlying content structure rather than actual content, providing context to organisms and showing how they interact.
    • Examples: A blog post template, a product listing template. These are often wireframes or abstract page layouts.
    • React Mapping: Page-level components that arrange Organisms, often passing down data fetched by their Container parent.
  5. Pages: Specific instances of templates, with real content populated into them. Pages are the highest level of fidelity and are where you see the design system truly come to life.
    • Examples: The actual homepage, a specific blog post, a specific product detail page.
    • React Mapping: The actual routes in your application (e.g., `/products/:id`, `/dashboard`). These often act as containers, fetching all necessary data and passing it down to the templates/organisms.

Benefits of Atomic Design: Provides a clear hierarchy for components, promotes consistency, enhances reusability, and makes it easier to manage a design system and its implementation in code. It provides a shared vocabulary between designers and developers.


Folder Structure for Large Applications

Choosing an effective folder structure is crucial for large-scale React applications. There are two main paradigms: grouping by type and grouping by feature. For larger applications, grouping by feature is generally preferred, often combined with a dedicated folder for common utilities and shared UI components.

1. Grouping by Feature (Recommended for Large Apps)

In this structure, all files related to a specific feature (components, Redux slices, hooks, utilities, styles) are kept together in a single folder. This makes it easy to find all relevant code for a feature and to delete or refactor features independently.

src/
├── app/                ┌── Core application setup (Redux store, hooks, routing config)
│   ├── store.ts
│   ├── hooks.ts
│   └── AppRouter.tsx
├── assets/             ┌── Images, fonts, static files
├── components/         ┌── Reusable presentational components (Atoms, Molecules from Atomic Design)
│   ├── Button/
│   │   ├── Button.tsx
│   │   └── Button.module.css
│   ├── UserAvatar/
│   │   ├── UserAvatar.tsx
│   │   └── UserAvatar.module.css
│   └── common-ui-library/ ┌── Potentially a separate UI component library
├── features/           ┌── Main features, grouped by business domain
│   ├── auth/              ┌── Authentication feature
│   │   ├── AuthPage.tsx       ┌── Feature's main component/page
│   │   ├── authSlice.ts     ┌── Redux slice for auth state
│   │   ├── authApi.ts       ┌── API calls related to auth
│   │   ├── LoginForm.tsx
│   │   └── RegisterForm.tsx
│   ├── products/          ┌── Products feature
│   │   ├── ProductsPage.tsx
│   │   ├── ProductCard.tsx
│   │   ├── productsSlice.ts
│   │   ├── productApi.ts
│   │   └── ProductDetail.tsx
│   └── cart/              ┌── Shopping cart feature
├── layouts/            ┌── Major page layouts (e.g., Header, Sidebar, Footer)
│   ├── MainLayout.tsx
│   └── AuthLayout.tsx
├── pages/              ┌── Top-level components that map to routes (often containers/templates)
│   ├── HomePage.tsx
│   ├── DashboardPage.tsx
│   └── NotFoundPage.tsx
├── services/           ┌── Generic API services or external integrations (e.g., axios config)
│   └── api.ts
├── utils/              ┌── Helper functions, common utilities
│   ├── helpers.ts
│   └── constants.ts
└── App.tsx             ┌── Main application component (often contains routing)
    

Key considerations for folder structure:

  • Consistency: Whatever structure you choose, apply it consistently throughout the project.
  • Scalability: Can it easily accommodate new features without becoming messy?
  • Discoverability: Can developers quickly find the code they need?
  • Maintainability: Is it easy to refactor or remove parts of the application?
  • Flat is better than deep: Avoid excessively deep nesting of folders.

This feature-based structure, combined with a clear distinction between common UI components (`components`) and domain-specific UI/logic (`features`), provides a highly scalable and maintainable foundation for large React applications. It aligns well with both the Container/Presentational pattern and Atomic Design by allowing you to define smaller, reusable UI pieces in `components` and then compose them into larger, feature-specific sections within `features` and `pages`.


References

następny o Using `fetch` API and `axios`. Integrating data fetching with Redux (e.g., using `createAsyncThunk`). Introduction to React Query or SWR for caching and synchronization (brief mention). Znakomicie! Czas na omówienie pobierania danych i integracji tego procesu z Reduxem, a także wprowadzenie do nowoczesnych bibliotek do zarządzania danymi. Oto artykuł na temat “Data Fetching and Redux Integration,” sformatowany jako kompletny dokument HTML, ze wszystkimi określonymi sekcjami i przykładami kodu. HTML

Data Fetching and Redux Integration in React Applications

Data Fetching and Redux Integration in React Applications

Building dynamic React applications invariably involves fetching data from external APIs. How you perform these network requests and how you integrate the fetched data into your application’s state management system (like Redux) are critical decisions that impact performance, maintainability, and user experience. This article will explore common methods for data fetching, demonstrate their integration with Redux using `createAsyncThunk`, and briefly introduce modern alternatives for managing server state.

Efficient data fetching and state synchronization are paramount for responsive user interfaces. Understanding these patterns is essential for any serious React developer, especially when dealing with complex data requirements in large-scale applications.


Using `fetch` API and `axios`

The two most common ways to make HTTP requests in modern JavaScript applications are the built-in `fetch` API and the third-party library `axios`.

1. The `fetch` API

The `fetch` API is a modern, promise-based API built directly into web browsers, providing a powerful and flexible way to make network requests. It’s available globally in the browser’s `window` object.

  • Built-in: No installation needed.
  • Promise-based: Uses Promises, making it easy to chain `.then()` and `.catch()` or use `async/await`.
  • Stream-oriented: Handles responses as streams, which can be beneficial for large data.
  • No automatic JSON parsing: You need to explicitly call `response.json()` (or `response.text()`, etc.) to parse the response body.
  • Error Handling: `fetch` only throws an error for network failures (e.g., no internet connection). It does *not* throw an error for HTTP error statuses (like 404 or 500); you must check `response.ok` or `response.status` manually.
async function fetchDataWithFetch(url: string) {
    try {
        const response = await fetch(url);

        if (!response.ok) { // Check for HTTP errors (e.g., 404, 500)
            const errorData = await response.json(); // Or response.text()
            throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || 'Unknown error'}`);
        }

        const data = await response.json(); // Parse JSON body
        console.log('Fetched data (fetch):', data);
        return data;
    } catch (error: any) {
        console.error('Fetch error:', error.message);
        throw error; // Re-throw to propagate error
    }
}

// Example usage:
// fetchDataWithFetch('https://jsonplaceholder.typicode.com/posts/1');
    

2. `axios`

`axios` is a popular, promise-based HTTP client for the browser and Node.js. It’s a third-party library that needs to be installed (`npm install axios`).

  • Feature-rich: Offers features like automatic JSON data transformation, request/response interceptors, cancellation, and built-in CSRF protection.
  • Automatic JSON parsing: Automatically parses JSON responses.
  • Error Handling: Throws an error for network issues *and* for HTTP error statuses (e.g., 404, 500), simplifying error handling.
  • Browser and Node.js support: Can be used in both environments.
import axios from 'axios'; // npm install axios

async function fetchDataWithAxios(url: string) {
    try {
        const response = await axios.get(url); // Axios automatically parses JSON and checks for errors
        console.log('Fetched data (axios):', response.data);
        return response.data;
    } catch (error: any) {
        if (axios.isAxiosError(error)) {
            console.error('Axios error:', error.message);
            if (error.response) {
                // Server responded with a status other than 2xx
                console.error('Response data:', error.response.data);
                console.error('Response status:', error.response.status);
            } else if (error.request) {
                // Request was made but no response received
                console.error('No response received:', error.request);
            }
        } else {
            // Something else happened
            console.error('Error:', error.message);
        }
        throw error; // Re-throw to propagate error
    }
}

// Example usage:
// fetchDataWithAxios('https://jsonplaceholder.typicode.com/todos/1');
    

Which to choose? `axios` is generally preferred in most professional React projects due to its rich feature set, simplified error handling, and ease of use. However, `fetch` is perfectly capable for simpler use cases or when you want to avoid an extra dependency.


Integrating Data Fetching with Redux (using `createAsyncThunk`)

When fetching data with Redux, you typically manage the lifecycle of the request (loading, success, error) within your store. Redux Toolkit’s `createAsyncThunk` is the recommended tool for this, as it automates the dispatching of pending, fulfilled, and rejected actions, streamlining the process.

Let’s use `axios` with `createAsyncThunk` to fetch a list of users and manage their state in Redux.

<!-- src/features/users/usersSlice.ts -->
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { RootState } from '../../app/store'; // Assuming RootState is exported from store.ts

interface User {
    id: number;
    name: string;
    email: string;
    // ... more user properties
}

interface UsersState {
    list: User[];
    status: 'idle' | 'loading' | 'succeeded' | 'failed';
    error: string | null;
}

const initialState: UsersState = {
    list: [],
    status: 'idle',
    error: null,
};

// Define the async thunk for fetching users
// Type arguments:
// 1. Return type of the payload creator (User[])
// 2. Argument type passed to the thunk (void, as we're not passing any)
// 3. Type for the ThunkAPI config (for getState, rejectWithValue etc.)
export const fetchUsers = createAsyncThunk<User[], void, { state: RootState, rejectValue: string }>(
    'users/fetchUsers', // Action type prefix
    async (_, { rejectWithValue }) => {
        try {
            const response = await axios.get<User[]>('https://jsonplaceholder.typicode.com/users');
            return response.data; // This becomes the action.payload for 'fulfilled'
        } catch (error: any) {
            // Return error message as the rejected value
            return rejectWithValue(error.response?.data?.message || error.message);
        }
    }
);

const usersSlice = createSlice({
    name: 'users',
    initialState,
    reducers: {
        // Synchronous reducers go here if needed
        clearUsers: (state) => {
            state.list = [];
            state.status = 'idle';
            state.error = null;
        }
    },
    // extraReducers handle actions dispatched by createAsyncThunk
    extraReducers: (builder) => {
        builder
            .addCase(fetchUsers.pending, (state) => {
                state.status = 'loading';
            })
            .addCase(fetchUsers.fulfilled, (state, action: PayloadAction<User[]>) => {
                state.status = 'succeeded';
                state.list = action.payload; // Update state with fetched users
            })
            .addCase(fetchUsers.rejected, (state, action: PayloadAction<string | undefined>) => {
                state.status = 'failed';
                state.error = action.payload || 'Failed to fetch users'; // Use rejected value as error
            });
    },
});

export const { clearUsers } = usersSlice.actions;
export default usersSlice.reducer;

// --- How to integrate into your store (src/app/store.ts) ---
// import { configureStore } from '@reduxjs/toolkit';
// import usersReducer from '../features/users/usersSlice';
// export const store = configureStore({
//     reducer: {
//         users: usersReducer, // Add your users slice
//     },
// });

// --- How to use in a React component (src/features/users/UserList.tsx) ---
// import React, { useEffect } from 'react';
// import { useAppSelector, useAppDispatch } from '../../app/hooks';
// import { fetchUsers, clearUsers } from './usersSlice';

// const UserList: React.FC = () => {
//     const dispatch = useAppDispatch();
//     const users = useAppSelector((state) => state.users.list);
//     const status = useAppSelector((state) => state.users.status);
//     const error = useAppSelector((state) => state.users.error);

//     useEffect(() => {
//         // Fetch users only once when component mounts and status is idle
//         if (status === 'idle') {
//             dispatch(fetchUsers());
//         }
//     }, [status, dispatch]);

//     if (status === 'loading') return <div>Loading users...</div>;
//     if (status === 'failed') return <div>Error: {error}</div>;
//     if (users.length === 0 && status === 'succeeded') return <div>No users found.</div>;

//     return (
//         <div>
//             <h2>Users List</h2>
//             <button onClick={() => dispatch(clearUsers())}>Clear Users</button>
//             <ul>
//                 {users.map((user) => (
//                     <li key={user.id}>{user.name} ({user.email})</li>
//                 ))}
//             </ul>
//         </div>
//     );
// };

// export default UserList;
    

This pattern makes your Redux store a robust handler for application state, including the state of your network requests (loading, data, error). However, for complex scenarios involving caching, revalidation, and background fetching, dedicated libraries offer more specialized solutions.


Introduction to React Query or SWR for Caching and Synchronization (Brief Mention)

While `createAsyncThunk` with Redux is excellent for managing global application state, handling server state (data fetched from APIs) often comes with unique challenges: caching, revalidation, synchronization across multiple components, retries, pagination, etc.

Libraries like React Query (now TanStack Query) and SWR (Stale-While-Revalidate) are specifically designed to address these problems. They are not replacements for Redux; rather, they are complementary tools that specialize in “server state” management, allowing Redux to focus on “client state” (UI state, form state, etc.).

Key Benefits of React Query / SWR:

  • Automatic Caching: Automatically cache fetched data, reducing unnecessary network requests.
  • Background Revalidation: Keep data fresh in the background, providing an “always up-to-date” feel.
  • Optimistic Updates: Provide instant UI feedback even before a server response, improving perceived performance.
  • Deduplication of Requests: Prevents multiple identical requests from being sent simultaneously.
  • Automatic Retries: Handle transient network failures gracefully.
  • Built-in Loading/Error States: Simplify managing loading, error, and success states directly in your components.
  • DevTools: Both offer excellent DevTools for inspecting cached data and queries.

When to use them:

  • For almost all data fetching from APIs.
  • When you need advanced caching, revalidation, and synchronization features out of the box.
  • To simplify complex data fetching logic in your components.

Example (Conceptual using React Query):

<!-- Conceptual example, not a full implementation -->
import React from 'react';
import { useQuery } from '@tanstack/react-query'; // npm install @tanstack/react-query
import axios from 'axios';

interface User {
    id: number;
    name: string;
    email: string;
}

const fetchUserById = async (userId: number): Promise<User> => {
    const { data } = await axios.get<User>(`https://jsonplaceholder.typicode.com/users/${userId}`);
    return data;
};

const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
    // useQuery takes a query key (unique identifier) and a query function
    const { data: user, isLoading, isError, error } = useQuery<User, Error>({
        queryKey: ['user', userId], // Cache key for this specific user
        queryFn: () => fetchUserById(userId),
        staleTime: 5 * 60 * 1000, // Data is considered fresh for 5 minutes
        // ... more configuration options
    });

    if (isLoading) return <div>Loading user profile...</div>;
    if (isError) return <div>Error: {error.message}</div>;
    if (!user) return <div>User not found.</div>;

    return (
        <div>
            <h2>User Profile</h2>
            <p>Name: {user.name}</p>
            <p;Email: {user.email}</p>
        </div>
    );
};

// To make it work, you'd wrap your app with QueryClientProvider:
// import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// const queryClient = new QueryClient();
// root.render(
//   <QueryClientProvider client={queryClient}>
//     <App />
//   </QueryClientProvider>
// );
    

Using React Query or SWR alongside Redux allows you to delegate “server state” management to specialized tools, simplifying your Redux store and focusing its use on true “client state.” This leads to a cleaner, more performant, and more scalable application architecture.


References

You may also like