Prop Design — Required Props, Defaults and PropTypes

Well-designed props are a component’s public API — they tell consumers exactly what data a component needs, which fields are required, and what the defaults are. React’s PropTypes library validates props at runtime in development and logs warnings when violations occur. While TypeScript’s static typing is more powerful for larger projects, PropTypes provides immediate value in plain JavaScript React applications with zero tooling overhead. Designing prop interfaces thoughtfully prevents runtime errors and makes components self-documenting.

PropTypes Basics

npm install prop-types
import PropTypes from "prop-types";

function PostCard({ post, onLike, compact = false }) {
    return (
        <article>
            <h2>{post.title}</h2>
            {!compact && <p>{post.body}</p>}
            <button onClick={() => onLike(post.id)}>Like</button>
        </article>
    );
}

// Prop type declarations — checked in development, stripped in production
PostCard.propTypes = {
    post: PropTypes.shape({
        id:         PropTypes.number.isRequired,
        title:      PropTypes.string.isRequired,
        body:       PropTypes.string,
        status:     PropTypes.oneOf(["draft", "published", "archived"]).isRequired,
        view_count: PropTypes.number,
        author:     PropTypes.shape({
            id:         PropTypes.number.isRequired,
            name:       PropTypes.string.isRequired,
            avatar_url: PropTypes.string,
        }).isRequired,
        tags: PropTypes.arrayOf(
            PropTypes.shape({
                id:   PropTypes.number.isRequired,
                name: PropTypes.string.isRequired,
            })
        ),
    }).isRequired,
    onLike:  PropTypes.func.isRequired,
    compact: PropTypes.bool,
};

PostCard.defaultProps = {
    compact: false,
};
Note: PropTypes validation only runs in development (process.env.NODE_ENV !== "production"). In production builds, Vite strips PropTypes checks, so there is zero runtime cost. The warnings appear in the browser console in development: “Warning: Failed prop type: The prop ‘post’ is marked as required in ‘PostCard’, but its value is undefined.” These warnings are invaluable for catching integration bugs early — treat them as errors, not suggestions.
Tip: For new React projects, consider TypeScript instead of PropTypes — TypeScript’s interface-based typing catches errors at compile time rather than runtime, works with IDEs for auto-complete, and scales better as the project grows. The blog application in this series uses plain JavaScript with PropTypes to stay accessible, but Chapter 39 notes where TypeScript interfaces would replace PropTypes in a production application.
Warning: PropTypes only validate the shape of props at the top level of a component — they do not deeply validate nested object mutations. A component can receive a correctly-shaped post object and then mutate it (post.title = "changed") without any PropTypes warning. PropTypes validates that you passed the right structure, not how you use it. Use PropTypes for documentation and basic type safety; rely on immutability discipline and code review for mutation safety.

Common PropTypes Patterns

import PropTypes from "prop-types";

// ── String with allowed values (enum) ─────────────────────────────────────────
Button.propTypes = {
    variant: PropTypes.oneOf(["primary", "secondary", "danger", "ghost"]),
    size:    PropTypes.oneOf(["sm", "md", "lg"]),
};

// ── Array of specific shape ────────────────────────────────────────────────────
PostList.propTypes = {
    posts: PropTypes.arrayOf(
        PropTypes.shape({
            id:    PropTypes.number.isRequired,
            title: PropTypes.string.isRequired,
        })
    ).isRequired,
};

// ── Callback functions ─────────────────────────────────────────────────────────
Modal.propTypes = {
    isOpen:   PropTypes.bool.isRequired,
    onClose:  PropTypes.func.isRequired,
    onSubmit: PropTypes.func,
    title:    PropTypes.string,
    children: PropTypes.node.isRequired,   // node = any renderable React content
};

// ── Union types ────────────────────────────────────────────────────────────────
Avatar.propTypes = {
    src: PropTypes.oneOfType([PropTypes.string, PropTypes.null]),
};

// ── Custom validator ───────────────────────────────────────────────────────────
Input.propTypes = {
    value: function(props, propName, componentName) {
        if (typeof props[propName] !== "string" && typeof props[propName] !== "number") {
            return new Error(`Invalid prop ${propName} in ${componentName}: expected string or number`);
        }
    },
};

Default Prop Values

// ── Method 1: defaultProps (explicit, easy to see all defaults at once) ───────
Avatar.defaultProps = {
    size:   40,
    src:    null,
};

// ── Method 2: destructuring defaults (co-located with parameter, preferred) ───
function Avatar({ src = null, name, size = 40 }) {
    // size defaults to 40 if not provided
    // src defaults to null (will show fallback)
}

// Both are equivalent — use destructuring defaults for new code (React team recommendation)

// ── Default for complex props ──────────────────────────────────────────────────
function PostList({ posts = [], isLoading = false, emptyMessage = "No posts found" }) {
    if (isLoading) return <Skeleton />;
    if (posts.length === 0) return <p>{emptyMessage}</p>;
    return posts.map(p => <PostCard key={p.id} post={p} />);
}

Common Mistakes

Mistake 1 — Not marking required props with .isRequired

❌ Wrong — silent failure when post is undefined:

PostCard.propTypes = {
    post: PropTypes.shape({ title: PropTypes.string }),   // not isRequired!
};
// <PostCard /> renders — no warning, then crashes on post.title access

✅ Correct — mark required props explicitly:

PostCard.propTypes = {
    post: PropTypes.shape({ ... }).isRequired,   // ✓ warns in dev if missing

Mistake 2 — Using PropTypes.object instead of PropTypes.shape

❌ Wrong — validates the type but not the structure:

PostCard.propTypes = { post: PropTypes.object.isRequired };
// {title: 123, id: "wrong_type"} passes — no validation of fields!

✅ Correct — use PropTypes.shape to validate each field.

Mistake 3 — defaultProps for required props

❌ Wrong — defaultProps on a required prop is contradictory:

Component.propTypes   = { title: PropTypes.string.isRequired };
Component.defaultProps = { title: "Untitled" };   // why require it if there's a default?

✅ Correct — either mark as optional (no .isRequired) with a default, or mark as required with no default.

Quick Reference

PropTypes Type Example
String PropTypes.string.isRequired
Number PropTypes.number
Boolean PropTypes.bool
Function PropTypes.func.isRequired
Object shape PropTypes.shape({ id: PropTypes.number })
Array of shape PropTypes.arrayOf(PropTypes.shape({...}))
Enum PropTypes.oneOf(["a", "b", "c"])
Any renderable PropTypes.node
React element PropTypes.element

🧠 Test Yourself

You declare onLike: PropTypes.func.isRequired but forget to pass it when using <PostCard post={post} />. When does the PropTypes warning appear?