Blog

April 2, 2026 · 7 min read

Why Your Data Belongs in lib/ Not in Your Components

A component that owns its own data is a component you cannot reuse. Separating data into a lib/ folder keeps components pure, makes data accessible anywhere, and makes both independently testable.

The Problem Starts Small

It starts reasonably enough. You need to render a list of services on the homepage. You write the array directly in the component — it is faster, it works, and there is no reason yet to do otherwise.

// components/ServicesList.tsx
export function ServicesList() {
    const services = [
        { title: 'React Development',  price: '€85/hr', slug: 'react-nextjs-development' },
        { title: '.NET Backend',       price: '€90/hr', slug: 'dotnet-backend' },
        { title: 'Full-Stack Project', price: '€80/hr', slug: 'fullstack-web-development' },
    ];

    return (
        <ul className="space-y-4">
            {services.map((s) => (
                <li key={s.slug}>
                    <span className="font-semibold">{s.title}</span>
                    <span className="text-gray-500 ml-2">{s.price}</span>
                </li>
            ))}
        </ul>
    );
}

Three months later, the services page needs the same list. The contact page needs the slugs to build links. A sitemap generator needs the slugs too. The data now lives in one component, but four parts of the application depend on it.

You have two options: copy the array into each place that needs it, or refactor under pressure. Neither is a good outcome for a problem that was avoidable from the start.

The Rule: Data and Presentation Are Different Concerns

A component has one job: take data and render it. Defining the data it renders is a separate job, and mixing both into the same file creates coupling that limits everything the component could otherwise do.

When data lives inside a component:

  • The data is only accessible to that component and its children
  • Testing the data requires rendering the component
  • Changing the data means knowing which component owns it
  • Reusing the data in a new context means duplicating it or refactoring

When data lives in lib/:

  • Any file in the project can import it with a single line
  • The data can be tested directly without rendering anything
  • Changing the data means editing one file regardless of how many places use it
  • Adding a new consumer — a new page, a new component, a sitemap — is one import

The lib/ Pattern in Practice

Move the data out of the component and into a typed file in lib/:

// lib/services.ts
export type Service = {
    title:       string;
    slug:        string;
    price:       string;
    description: string;
    tags:        string[];
};

export const services: Service[] = [
    {
        title:       'React & Next.js Development',
        slug:        'react-nextjs-development',
        price:       '€85/hr',
        description: 'Fast, SEO-friendly web apps with TypeScript and Tailwind CSS.',
        tags:        ['React', 'Next.js', 'TypeScript', 'Tailwind CSS'],
    },
    {
        title:       '.NET Backend Development',
        slug:        'dotnet-backend',
        price:       '€90/hr',
        description: 'Production-grade REST APIs and background services with ASP.NET Core.',
        tags:        ['C#', '.NET', 'ASP.NET Core', 'PostgreSQL'],
    },
    {
        title:       'Full-Stack Web Development',
        slug:        'fullstack-web-development',
        price:       '€80/hr',
        description: 'End-to-end web applications — frontend, backend, database, and deployment.',
        tags:        ['React', 'Next.js', '.NET', 'PostgreSQL'],
    },
];

The component becomes a pure rendering function:

// components/ServicesList.tsx
import { services } from '@/lib/services';

export function ServicesList() {
    return (
        <ul className="space-y-4">
            {services.map((s) => (
                <li key={s.slug}>
                    <span className="font-semibold">{s.title}</span>
                    <span className="text-gray-500 ml-2">{s.price}</span>
                </li>
            ))}
        </ul>
    );
}

Now every consumer that needs service data imports from the same source:

// app/services/page.tsx — renders the full services page
import { services } from '@/lib/services';

// app/sitemap.ts — generates sitemap entries for each service
import { services } from '@/lib/services';

const serviceRoutes = services.map((s) => ({
    url: `https://eliezerkibet.dev/services/${s.slug}`,
    lastModified: new Date(),
    priority: 0.8,
}));

// components/home/ServicesPreview.tsx — teaser on the homepage
import { services } from '@/lib/services';

const featured = services.slice(0, 2);

Four consumers. One source of truth. Changing a price, a slug, or a description happens in one file and propagates everywhere instantly.

Adding Utility Functions Alongside the Data

Once data lives in lib/, it is straightforward to add utility functions that operate on it. These functions stay with the data they work on, not scattered across components:

// lib/services.ts — data and utilities in the same file
export function getServiceBySlug(slug: string): Service | undefined {
    return services.find((s) => s.slug === slug);
}

export function getServicesByTag(tag: string): Service[] {
    return services.filter((s) => s.tags.includes(tag));
}

export function getAllServiceSlugs(): string[] {
    return services.map((s) => s.slug);
}
// app/services/[slug]/page.tsx
import { getServiceBySlug, getAllServiceSlugs } from '@/lib/services';

export async function generateStaticParams() {
    return getAllServiceSlugs().map((slug) => ({ slug }));
}

export default function ServicePage({ params }: { params: { slug: string } }) {
    const service = getServiceBySlug(params.slug);
    if (!service) notFound();
    // ...
}

The page component has no awareness of the data structure — it calls a function, gets a typed result, and renders it. The routing logic, the data shape, and the rendering logic are each in exactly one place.

Typed Data Catches Errors Before Runtime

Moving data to lib/ and giving it a TypeScript type means the compiler validates every consumer. If you rename a field in the Service type, every file that accesses the old field name shows a type error immediately — in your editor, before you run the app.

// Change the type
export type Service = {
    title:       string;
    path:        string; // renamed from 'slug'
    price:       string;
    description: string;
    tags:        string[];
};

// Every file that used service.slug now shows a type error
// TypeScript finds them all — you cannot miss one

With data scattered across components, the same rename requires a manual search-and-replace and hoping you found every instance. With data in lib/ and a shared type, TypeScript does the audit for you.

The Component Becomes Truly Reusable

A component that imports its data directly from lib/ is still coupled to that specific dataset. The next level of reusability is making the component accept data as props — letting the caller decide what data to pass:

// components/ServiceCard.tsx — accepts data as props, knows nothing about lib/
interface ServiceCardProps {
    title:       string;
    description: string;
    price:       string;
    slug:        string;
}

export function ServiceCard({ title, description, price, slug }: ServiceCardProps) {
    return (
        <div className="p-6 bg-white rounded-xl border border-gray-200">
            <h3 className="font-bold text-gray-900 mb-2">{title}</h3>
            <p className="text-sm text-gray-600 mb-4">{description}</p>
            <div className="flex items-center justify-between">
                <span className="text-primary-600 font-semibold">{price}</span>
                <a href={`/services/${slug}`} className="text-sm font-medium">Learn more →</a>
            </div>
        </div>
    );
}
// app/services/page.tsx — data comes from lib/, rendering handled by component
import { services } from '@/lib/services';
import { ServiceCard } from '@/components/ServiceCard';

export default function ServicesPage() {
    return (
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
            {services.map((service) => (
                <ServiceCard key={service.slug} {...service} />
            ))}
        </div>
    );
}

Now ServiceCard can render any service-shaped data — from lib/services.ts, from an API response, from a test fixture. It has no dependencies outside of React. It is straightforward to test in isolation, straightforward to use in Storybook, and straightforward to reuse in a different part of the application.

What Belongs in lib/

The lib/ folder is not just for static data arrays. It is the right home for any logic or data that is independent of the UI layer:

  • Static data — services, projects, team members, navigation items, FAQs
  • Data fetching functions — functions that call APIs or query a database
  • Data transformation utilities — formatters, sorters, filters that operate on domain data
  • Type definitions — shared types used across multiple components or pages
  • Constants — base URLs, configuration values, feature flags
  • Validation schemas — Zod schemas used in both form validation and API input validation
lib/
├── blog.ts          // blog post type, getAllBlogPosts(), getBlogPostBySlug()
├── projects.ts      // project type, getAllProjects(), getProjectBySlug()
├── services.ts      // service type, services array, getServiceBySlug()
├── constants.ts     // BASE_URL, SITE_NAME, CONTACT_EMAIL
├── utils.ts         // formatDate(), truncate(), cn() class merger
└── posts/           // individual blog post files
    ├── post-one.ts
    └── post-two.ts

The Maintenance Argument

The real value of this structure becomes clear when you need to make changes. Consider a requirement to add a new field — an estimated delivery time — to every service:

With data in components, this change requires:

  1. Finding every component that defines service data
  2. Updating each array with the new field
  3. Updating each component's rendering logic
  4. Hoping you found all of them

With data in lib/services.ts:

  1. Add the field to the Service type
  2. TypeScript immediately shows every component that needs to handle the new field
  3. Add the values to the data array
  4. Update the components the type errors pointed to

The second approach is faster, safer, and leaves no room for missed updates. The structure enforces correctness — not because of discipline, but because the type system makes incomplete changes visible immediately.

Summary

Keeping data in lib/ and components in components/ is not a convention for large teams with strict architecture standards. It is a practical habit that pays off the first time you need the same data in two places, the first time you rename a field, and the first time a new developer joins the project and needs to understand where things are.

Components render. lib/ provides. Keeping those responsibilities separate is one of the highest-leverage structural decisions in a React or Next.js project.

For a related look at how reusable components are structured in Next.js projects, see the post on building reusable components in Next.js. For how this pattern applies to Tailwind-based styling, see Tailwind CSS vs global CSS. If you are building a React or Next.js application and want the architecture set up correctly from the start, see the React and Next.js development services page or get in touch.