DEV Community

Denys
Denys

Posted on • Edited on

1 1

Learn React composition in 15 minutes

Motivation

I used to use React UI libraries such as MUI or Chakra or something else. Some of these libraries create components in a composition way, so today I want to describe the way how you can do it in your project with your own realization.

Let's grab some coffee and let's go.

Sorry for the gif, though :D

Prerequisites

  • React
  • coffee || tea
  • good attitude

Composition

A couple of words about composition, simple explanation: combining smaller, independent components to create complex UIs.

Why?

From the explanation, combining some components to create a complex UI, or, in some cases, creating an independent group of components to handle its ecosystem.

Simple composition example

So firstly we need to run a new react project. The way I done with it:

  • yarn init -y
  • yarn add react react-dom vite
  • yarn add -D typescript @types/react-dom @types/react

Then I created a typescript config file:
tsconfig.json

{
    "compilerOptions": {
        "outDir": "build/dist",
        "module": "NodeNext",
        "target": "es6",
        "lib": ["es6", "dom"],
        "jsx": "react-jsx",
        "moduleResolution": "NodeNext",
        "rootDir": "src",
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noImplicitAny": true,
        "strictNullChecks": true
    },
    "exclude": ["node_modules", "build"],
    "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

and index.html in the root of repository, following vite doc
index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React Composition Example</title>
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/index.tsx"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

and also the index.tsx file to run our React
index.tsx

import { createRoot } from 'react-dom/client'
import { App } from './App'

const root = createRoot(document.getElementById('root') as HTMLDivElement)
root.render(<App />)
Enter fullscreen mode Exit fullscreen mode

and the App.tsx as an entry point for our app.
App.tsx

import React from 'react'
export const App = () => {
    return (
        <>
           Hi there!
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

So now we can add a script to the package.json and run the app:
package.json

//...
  "scripts": {
    "dev": "vite"
  }
//...
Enter fullscreen mode Exit fullscreen mode

So where are we? We created a simple react app with vite and we can run it.

Now we need to create a folder, I named Composition, where we will store all our composition files.

I created a simple types.ts file for shared types between composition files.
types.ts

import { FC, PropsWithChildren } from 'react'

export type FCWithChildren<T> = FC<PropsWithChildren<T>>
Enter fullscreen mode Exit fullscreen mode

just for having children with FC type.

Then I created 3 components, Head, Footer, Body and Wrapper.

I will share it further, but for now, we need to create a main logic with Provider and Context itself.

So, I created a file called Context.tsx
Context.tsx

import { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'
import { FCWithChildren } from './types'

interface ContextState {
    initialContext: boolean
    data: unknown[]
    setData: Dispatch<SetStateAction<ContextState['data']>>
}

const CompositionContext = createContext<ContextState>({ initialContext: true } as ContextState)

export const useCompositionContext = () => {
    const context = useContext(CompositionContext)

    if (context.initialContext) {
        throw new Error('Use context inside provider.')
    }

    return context
}

export const CompositionContextProvider: FCWithChildren<unknown> = ({ children }) => {
    const [data, setData] = useState<unknown[]>([])
    return (
        <CompositionContext.Provider value={{ initialContext: false, data, setData }}>
            {children}
        </CompositionContext.Provider>
    )
}
Enter fullscreen mode Exit fullscreen mode

The key points of this file are:

  • this file has an interface for context
  • this file has CompositionContextProvider to provide the context to nested components
  • this file has useCompositionContext function, which we will invoke in our nested under CompositionContextProvider components
  • simple condition statement, but I will describe it more little bit later

So now time to create nested components to use the context.
Head.tsx

import React, { FC } from 'react'
import { useCompositionContext } from './Context'

export const Head: FC<{ order?: number }> = ({ order }) => {
    const context = useCompositionContext()

    return <div>Head</div>
}
Enter fullscreen mode Exit fullscreen mode

Footer.tsx

import React from 'react'
import { useCompositionContext } from './Context'

export const Footer = () => {
    const context = useCompositionContext()
    return <div>Footer</div>
}
Enter fullscreen mode Exit fullscreen mode

Body.tsx

import React from 'react'
import { useCompositionContext } from './Context'

export const Body = () => {
    const context = useCompositionContext()
    return <div>Body</div>
}
Enter fullscreen mode Exit fullscreen mode

Also, I created an index.tsx for re-exporting our composition and assigning it to the constant.
Composition/index.tsx

import { Body } from './Body'
import { Footer } from './Footer'
import { Head } from './Head'
import { Wrapper } from './Wrapper'

export const Composition = { Body, Footer, Head, Wrapper }
Enter fullscreen mode Exit fullscreen mode

Also, I created a Wrapper.tsx file to compose our components with context.
Wrapper.tsx

import React from 'react'
import { FCWithChildren } from './types'
import { CompositionContextProvider } from './Context'

export const Wrapper: FCWithChildren<unknown> = ({ children }) => {
    return <CompositionContextProvider>{children}</CompositionContextProvider>
}

Enter fullscreen mode Exit fullscreen mode

and also the last one Layout.tsx to render our components:
Layout.tsx

import React from 'react'
import { Composition } from '.'

export const CompositionLayout = () => {
    return (
        <Composition.Wrapper>
            <Composition.Head />
            <Composition.Body />
            <Composition.Footer />
        </Composition.Wrapper>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now it is time to modify App.tsx file to apply our changes from the composition.

App.tsx

import React from 'react'
import { CompositionLayout } from './Composition/CompositionLayout'

export const App = () => {
    return (
        <>
           <CompositionLayout />
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

As I said above we have the condition and now this is an explanation why.

This condition:

if (context.initialContext) {
        throw new Error('Use context inside provider.')
    }
Enter fullscreen mode Exit fullscreen mode

was about to prevent using components outside the provider,
so if we try to use it outside.

App.tsx

import React from 'react'
import { CompositionLayout } from './Composition/CompositionLayout'
import { Composition } from './Composition'

export const App = () => {
    return (
        <>
            <CompositionLayout />
            <Composition.Head />
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

we get an error from our throw new Error(...), cuz we trying to use it outside.

GitHub

Repository from article: https://github.com/lgtome/react-composition

Outro

I will be glad to see your comments, some enhancements, questions, and concerns.

AWS Q Developer image

Your AI Code Assistant

Generate and update README files, create data-flow diagrams, and keep your project fully documented. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →