DEV Community

Cover image for The Anatomy Of My Ideal React Component
Antonin J. (they/them)
Antonin J. (they/them)

Posted on

The Anatomy Of My Ideal React Component

import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import tw from 'twin.macro'

import { USER_ROUTES, useUser } from 'modules/auth'
import { Loader } from 'modules/ui'
import { usePost } from 'modules/posts'

import { EmptyFallback } from './emptyFallback'

const StyledContainer = styled.div`
  ${tw`w-100 m-auto`}
`

const StyledHeading = styled.h1`
  ${tw`text-lg`}
`

type PostProps = {
  id: string
}

export const Post = ({ id }: PostProps): JSX.Element => {
  const [isExpanded, setIsExpanded] = useState(false)

  const { isLoading, isSuccess, post } = usePost({ id })
  const { user } = useUser()

  if (isLoading) {
    return <Loader />
  }

  if (!isLoading && !post) {
    return <EmptyFallback />
  }

  return (
    <StyledContainer>
     <Link to={USER_ROUTES.ACCOUNT}>Back to account, {user.name}</Link>
     <StyledHeading>{post.title}</StyledHeading>
     {post.body}
    </StyledContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is how I write my components and how I prefer to write React. It's a super specific way that works for me - and that includes using styled-components. If you have suggestions on how to improve this structure, I'm all ears. I love to improve how I do things and I greatly enjoy feedback.

I'll drop questions in the article if you'd like to give me feedback on those!

For anyone new to React or JS or development or TS, don't even worry about all that this is doing. I really just wanted to showcase a complicated example.

Note: I wanted to stress this again -- this is what works for me. I'd love to hear about what works for you!

Imports

Does import order matter? Not really. But I like to have rules around them especially for bigger components that might have 20 lines of imports or more. And that happens more than I'd like to admit. My general heuristics are:

  1. React up top no matter what
  2. 3rd party library imports (followed by a new line)
  3. internal library imports (and aliased imports)
  4. local imports
// react
import React, { useEffect } from 'react'

// 3rd party libraries
import moment from 'moment'
import styled from 'styled-components'

// internal shared components/utils/libraries
import { ListItems, useItems } from 'modules/ui'

// local
import { EmptyFallback } from './EmptyFallback'
Enter fullscreen mode Exit fullscreen mode

Why? When you deal with more than a handful of imports, it's really easy to get lost in what the file is using. Having conventions around imports makes it easier for me to see what it's using at a glance

Styled Components

No matter what library you use, you are writing your CSS somewhere. I'm a fan of styled-components (we use them at work) and Tailwind (I use it in personal projects). Twin allows you to combine them together -- that way you can write custom CSS if you need to, and Tailwind is great for rapid prototyping and production-ready apps alike. Best of both worlds.

I put these at the top because my components below typically use them. If there are too many styled components, I tend to put them in a co-located styled.ts file.

I also tend to prefix styled components with Styled. Something I learned at work. It quickly distinguishes between styling components and components that do more than that.

const StyledContainer = styled.div`
  ${tw`w-full`}

  background-color: ${COLORS.CONTAINER_BACKGROUND};
`

export const SomeComponent = () => {
  // logic
  const items = useItems()

  return (
   <StyledContainer> {/* styled component that does nothing else */}
    <List items={items} /> {/* component with internal logic */}
   </StyledContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

Why? I prefer co-located styling but also, put these up top so they're separate from components that have more than styling.

Questions for you: How do you organize your styled components? If you have a file with styled components, what do you call it?

Component Types

I typically name my Component types as ComponentNameProps and ComponentNameReturn where most of the time, I skip the "return" to use JSX.Element (I do use the Return type for hooks though! I'll write about that another day). Check out the React TypeScript CheatSheet which contains majority of the conventions I use for TypeScript and React.

This convention (naming and placement) makes it clear that:

  1. this type belongs to the component
  2. this type isn't shareable
  3. where to find the typing (right above the component)

It's also a stylistic choice not to inline it but you can:

// I don't like this
const SomeComponent = ({ 
  id,
  isEnabled,
  data,
  filter,
  onClick
}: {
  id: string,
  isEnabled: boolean
  data: DataStructureType
  filter: FilterType
  onClick: () => void
}): JSX.Element => {}

// I do like this
type SomeComponentProps = {
  id: string,
  isEnabled: boolean
  data: DataStructureType
  filter: FilterType
  onClick: () => void
}

const SomeComponent = ({ 
  id,
  isEnabled,
  data,
  filter,
  onClick
}: SomeComponentProps): JSX.Element => {}
Enter fullscreen mode Exit fullscreen mode

I feel like I have to constantly re-emphasize: this is what works for me specifically. There's no science or research behind this. It's not "easier to reason about" (which most of the time means "I like this", anyways).

Why? Co-located component types right above the component declaration make it easier for me to see the Component's purpose and signature at a glance

Questions for you: Do you co-locate your component types? Do you prefer inling them?

Component structure

Ok, let's dig into the Component structure. I think components typically have the following parts to them (some more or less, depending on what you're doing):

  1. local state (useState, useReducer, useRef, useMemo, etc.)
  2. non-React hooks and async/state fetching stuff (react-query, apollo, custom hooks, etc.)
  3. useEffect/useLayoutEffect
  4. post-processing the setup
  5. callbacks/handlers
  6. branching path rendering (loading screen, empty screen, error screen)
  7. default/success rendering

More or less, but let's go through them:

// local state
const [isExpanded, setIsExpanded] = useState(false)

// non-react hooks
const { isLoading, post } = usePost({ id })

// useEffect
useEffect(() => {
  setIsExpanded(false) // close expanded section when the post id changes
}, [id])

// post processing
const snippet = generateSnippet(post)

// callbacks and handlers
const toggleExpanded = (e: Event): void => {
  setIsExpanded((isExpanded) => !isExpanded)
}

// branching path rendering
if (isLoading) {
  return <Loading />
}

if (post && !isExpanded) {
  return (
    <StyledContainer>{snippet}</StyledContainer>
  )
}

// default/success render
return <StyledContainer>
  <h1>{post.title}</h1>
  <div>{post.content}</div>
</StyledContainer>
Enter fullscreen mode Exit fullscreen mode

So a few things about this, I set this up so that the logic seems to flow down and we declare as much ahead of time as possible. I think there is quite a bit of wiggle space here because what really matters is declaring variables and using hooks before we render. This is necessary for hooks to work right. If you try to short-circuit a render and skip a hook as a result, React will let you know that's an issue.

I also like to add the handler at the end of that declaration block so I have access to any variables I might need if I convert it to use useCallback. Which is also why I use const func = () => {} instead of function func() {} -- to quickly convert to useCallback and to avoid a mismatch of named functions and lambdas.

We can then safely jump into branching path rendering for loading screens, errors, etc. without worrying about hooks. We can exit the render safely early this way.

And lastly, I keep the default/success render at the bottom.

Why It's a lot to cover but honestly, it's all personal preference influenced by React's requirement to always run all hooks in a component.

Question Does this differ much from your standards?

Potential For Refactor

You might notice that my original component doesn't have a useEffect or the post-processing examples. Why is that?

Typically, if I have to do some lifting in a component to get data in a specific state, or I have variables that relate to each other, I like to hide that in a hook.

For example:

type UsePostProps = {
  id: string
}

type UsePostReturn = {
  isExpanded: boolean
  post: PostType
  isLoading: boolean
  toggleExpanded: () => void
}

export const usePost = ({ id }: UsePostProps): UsePostReturn => {
  const [isExpanded, setIsExpanded] = useState(false)
  const { isLoading, data } = useQuery('cache', getPost)

  useEffect(() => {
    setIsExpanded(false)
  }, [id])

  const post = !isLoading && formatPost(data)

  return {
   isExpanded,
   toggleExpanded,
   isLoading,
   post,
  }
}
Enter fullscreen mode Exit fullscreen mode

Why I'm not sure. I don't like big components, I don't like components that do too much so I split logic up into hooks however feels right to me.

Question How do you feel about wrapping API calls in custom hooks by default? eg. any unique call has its own hook that handles data transformation, related state, and the fetching. And do you prefer to have this logic inside of a component?

Wondering about folder structure?

I made a React application structure video on that topic. Though, in hindsight, it has a few syntax errors I didn't notice while recording.

Discussion (15)

Collapse
lukeshiru profile image
Luke Shiru

My approach for the same component:

First, I would move types to external files:

// This one would be in an external file like PostData.ts
type PostData = {
    readonly body: string;
    readonly title: string;
    readonly user: string;
};

// This one would be in an external file like PostProps.ts
type PostProps = JSX.IntrinsicElements["div"] & {
    readonly linkProps?: LinkProps;
    readonly loading?: boolean;
    readonly post?: PostData;
    readonly titleProps?: JSX.IntrinsicElements["h1"];
};
Enter fullscreen mode Exit fullscreen mode

Then the code for the actual component would look like this:

import classNames from "classnames";
import { USER_ROUTES } from "modules/auth";
import { Loader } from "modules/ui";
import type { FC } from "react";
import { Link } from "react-router-dom";
import { EmptyFallback } from "./emptyFallback";
import type { PostProps } from "./PostProps";

export const Post: FC<PostProps> = ({
    className,
    children,
    loading = false,
    post,
    linkProps,
    titleProps,
    ...props
}) =>
    loading ? (
        <Loader {...props} />
    ) : post ? (
        <div className={classNames("w-100 m-auto", className)} {...props}>
            {children ?? (
                <>
                    <Link to={USER_ROUTES.ACCOUNT} {...linkProps}>
                        {linkProps.children ?? `Back to account, ${post.user}`}
                    </Link>
                    <h1 className="text-lg" {...titleProps}>
                        {titleProps.children ?? post.title}
                    </h1>
                    {post.body}
                </>
            )}
        </div>
    ) : (
        <EmptyFallback {...props} />
    );
Enter fullscreen mode Exit fullscreen mode

So:

  • I wouldn't put state inside a component that's used to show data as Post, I would make it receive everything by props.
  • I wouldn't use a bunch of early returns with duplicated logic when a ternary can do the job.
  • I would pass ...props down to the underlying div, and combine it with the JSX.IntrinsicElements type, is very powerful, making the component more useful (you can set up stuff like aria labels on it).
  • I wouldn't use Tailwind AND Styled components. Is either one or the other, so in my case I would just use tailwind directly.
  • I would use the classnames package to combine the consumer's className with the preset classNames.
  • I wouldn't put everything in the same file, I would try to keep it leaner and just have the component in one place, types elsewhere.
  • I would add linkProps and titleProps so devs don't lose access to the underlying components if needed.

Hope this other approach helps get other perspectives for component dev.

Cheers!

Collapse
antjanus profile image
Antonin J. (they/them) Author

Thanks for the perspective! I find it interesting how everyone does things differently and their reasoning behind it. I do agree with the ...props and taking advantage of the HTML element typing. I typically do it for internal UI library work.

I'd love to hear more about the separation between components that show data and components that fetch it.

How do you structure components that do data-fetching and keep state?
Do you specifically avoid any presentational elements in those?

Collapse
lukeshiru profile image
Luke Shiru

I generally handle it with a "paired hook pattern". I'm working on an article about this pattern, but basically you create a stateless component, and then a hook that sets the properties for you, so you have the option of using the component with the hook, or keep it stateless. A simple example:

// Instead of doing this:
const Component = ({ id }) => {
  const { loading, data } = someAPIHook({ id });

  return loading ? "Loading..." : <div>{data}</div>;
};

// And use it like this:
<Component id="foo" />

// You do this:
const Component = ({ loading, data }) => loading ? "Loading..." : <div>{data}</div>

// And you can either use it like this:
const props = someAPIHook({ id });
// ...
<Component {...props} />

// Or you can use it like this:
const { data, loading } = someAPIHook({ id });
const [otherLoading, setOtherLoading] = useState(true);
// ...
<Component loading={loading && otherLoading} data={data} />
Enter fullscreen mode Exit fullscreen mode

The important thing is that you "broaden your options", so you can use it stateful, stateless, half and half, and so on. Hope that helps :)

Thread Thread
antjanus profile image
Antonin J. (they/them) Author

thanks! :)

Collapse
inviscidpixels profile image
aten

Styled Components are great.

While it might seem overkill, I find using useReducer (depending on how you implement it properly or not) (or in combination with your custom hook logic), because it conforms the code to a deterministic state machine (more than it doesn't) and setups UX as an event processing queue, is much cleaner and more than not leads to such.

Thanks for the article! Didn't know about Twin, best of both worlds!

Collapse
nevulo profile image
Nevulo

Great post, and completely agree with the 7 points on component structure you mentioned, makes for very readable components and higher quality code!

At the end you mention "not liking big components" and not being sure why, personally I try to follow the single-responsibility principle (I imagine following this principle is why splitting up components feels "right"), and making sure that any component or hook only has only one responsibility. Specifically, something like a Post component should only be worried about rendering the contents of a post, not things like the comments for simplicities sake.

The beautiful thing about components is the ability to use composition to build up bigger components from smaller ones, and having explicit, clear, simple components to build up more complicated ones makes the structure more understandable.

Collapse
well1791 profile image
Well

I love twin.macro but I'm more of a stitchesjs guy, which btw twin.macro already supports, and I'm going to provide an example for it.

Also, I believe there's a nicer approach to that repetitive conditional sequence if error then <Error /> else if isLoading then <Loading /> else if data then <Component /> else <Empty /> check redwoodjs approach redwoodjs.com/docs/cells, in short, you just have a wrapper smart enough to deal with all that logic.

I agreed with pretty much everything, but types must be moved in a separated file, also, the component tag type must be exported along with the component props (for a11y). And, when it comes to the folder structure, I prefer styles in a separated file over "styled-components" in the same file. Let me elaborate more on this.

/// folder structure
- src/components/Post/Post.tsx
- src/components/Post/PostStyles.ts
Enter fullscreen mode Exit fullscreen mode

where

/// src/components/Post/Post.tsx
import * as stl from './PostStyles'
import type { PostProps } from './PostTypes'

export const Post: React.FC<PostProps> = ({ data, ...p }: PostProps) => {
  return (
    <div {...p} className={stl.container({ className: p.className })}>
      ...
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Then all you have to do, is work your styles in a separated file!

/// src/components/Post/styles.ts
import { css } from 'src/shared/theme'

export const container = css({
  $$minHeight: 'inherit', // <- stitchesjs magic
  bg: '$blue100',
  h: '$$minHeight,
})
Enter fullscreen mode Exit fullscreen mode

Now! Where is the fun?

  1. Your Post component now displays only markup content! No need to have a bunch of "mini components" with styles listed in the same file.
  2. You can now see the html tag name instead of some random (and sometimes silly) name! Which means, now you have a better experience with a11y
  3. If for some reason you need to override the style of an inner component! You could just override it by using styles only! No need to pass some new props everywhere!
/// src/component/BigFun/styles.tsx
import * as postStl from 'src/components/Post/PostStyles'

export const container = css({
  [`.${postStl.container()}`]: {
    $$minHeight: '100%',
  },
})
Enter fullscreen mode Exit fullscreen mode

But! I know, I know, you're think "wtf? how do I change things programmatically in the PostStyles.ts file?" Aaaaand!! Here's where stitches shines: stitches.dev/docs/variants 🎉 🤷 everything is typed and everyone is happy! (for now... 🤨)

Collapse
antjanus profile image
Antonin J. (they/them) Author

After reading your comment about splitting everything up, I'm wondering if I'm mildly biased toward bigger components because I just legit use a bigger monitor 😂

Collapse
jerrylowm profile image
Jerry Low

This is amazing, because without any guidance or collaboration we’re almost doing the exact same thing. What many developers don’t see is the power of formatting, some small changes in grouping makes code so much more readable.

The only difference I have is that I like to deconstruct the props in the component, just in case it gets overwhelming (which hopefully it doesn’t.

Collapse
ceoshikhar profile image
Shikhar Sharma

I always read posts that Luke comments on. If he spent time on reading it, it probably is worth it.

This time, it's true again. Cheers!

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
antjanus profile image
Antonin J. (they/them) Author • Edited on

If you’re up for it, I’d love a code review! How would you do things? What do you see as messy code?

A couple of reasons for TW + styled components —

Tailwind doesn’t handle 100% of situations. Having both gives us more flexibility. Having them interoperate feels cleaner to me on top of it. You can also used styled components to conditionally generate scoped tailwind

And then Personal preference but I prefer to not have classNames and naked HTML in my components unless they’re specifically in like a shared UI library. I find it so much nicer to write <StyledContainer> than <div className="w-full"> in general.

Like I said at the top of the article, this is what works for me and vibes with me 🙂

I’d love to hear why not both, tbh! I was so excited to find Twin and follow this pattern!

Collapse
kevinletchford profile image
Kevin Letchford

This is a totally unproductive comment. No insight, just belittling OP with out any reasoning.

Collapse
topolanekmartin profile image
Martin Topolanek

Since we use ReturnType type we barely create Return type manually.

Take a look at this typescriptlang.org/docs/handbook/u...

Collapse
antjanus profile image
Antonin J. (they/them) Author

I really like this utility! I love that TS has so many of them.

I found typing return types manually to be a great way to document the intent of the return and being clear around what id like to return as I write the hook.

But I like the utility when I don’t want to export the type.