DEV Community

Denys
Denys

Posted on

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.

Top comments (0)