Learn React composition in 15 minutes
Tags: react, javascript, typescript, webdev
Published: 2024-05-30 20:43:05

## 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.

<img width="100%" style="width:100%" src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExM2ZwbnR3YW8zZ2ppNzlyMnVjdXk4aWw1a3ZzZGxldzlvbGo3cW15OCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3o6ozw0cY70PJXQGBO/giphy.gif">

_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**
```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/**/*"]
}
```
and `index.html` in the root of repository, following `vite` doc
**index.html**
```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>
```
and also the `index.tsx` file to run our `React`
**index.tsx**
```ts
import { createRoot } from 'react-dom/client'
import { App } from './App'

const root = createRoot(document.getElementById('root') as HTMLDivElement)
root.render(<App />)
```
and the `App.tsx` as an entry point for our app.
**App.tsx**
```ts
import React from 'react'
export const App = () => {
    return (
        <>
           Hi there!
        </>
    )
}

```

So now we can add a script to the `package.json` and run the app:
**package.json**
```json
//...
  "scripts": {
    "dev": "vite"
  }
//...
```

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**
```ts
import { FC, PropsWithChildren } from 'react'

export type FCWithChildren<T> = FC<PropsWithChildren<T>>
```
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**
```ts
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>
    )
}
```

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

<img width="100%" style="width:100%" src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExM2tidGdpdTdhcnJoeml5Mzh0cHFteGtpZWViazQ1M3VjNWQ2MGJzZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/5Jnzd5fuvipfZa4797/giphy.gif">

So now time to create nested components to use the context.
**Head.tsx**
```ts
import React, { FC } from 'react'
import { useCompositionContext } from './Context'

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

    return <div>Head</div>
}
```
**Footer.tsx**
```ts
import React from 'react'
import { useCompositionContext } from './Context'

export const Footer = () => {
    const context = useCompositionContext()
    return <div>Footer</div>
}
```
**Body.tsx**
```ts
import React from 'react'
import { useCompositionContext } from './Context'

export const Body = () => {
    const context = useCompositionContext()
    return <div>Body</div>
}
```

Also, I created an `index.tsx` for re-exporting our composition and assigning it to the constant.
**Composition/index.tsx**
```ts
import { Body } from './Body'
import { Footer } from './Footer'
import { Head } from './Head'
import { Wrapper } from './Wrapper'

export const Composition = { Body, Footer, Head, Wrapper }
```

Also, I created a `Wrapper.tsx` file to compose our components with context.
**Wrapper.tsx**
```ts
import React from 'react'
import { FCWithChildren } from './types'
import { CompositionContextProvider } from './Context'

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

```

and also the last one `Layout.tsx` to render our components:
**Layout.tsx**
```ts
import React from 'react'
import { Composition } from '.'

export const CompositionLayout = () => {
    return (
        <Composition.Wrapper>
            <Composition.Head />
            <Composition.Body />
            <Composition.Footer />
        </Composition.Wrapper>
    )
}
```

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

**App.tsx**
```ts
import React from 'react'
import { CompositionLayout } from './Composition/CompositionLayout'

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

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

This condition:
```ts
if (context.initialContext) {
        throw new Error('Use context inside provider.')
    }
```
was about to prevent using components outside the provider,
so if we try to use it outside.

<img width="100%" style="width:100%" src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMWZuODdscmNvcHJmYmZpZXN3bW5sb3p2NnE5Mjg3MmI0dmtiNmtzaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/daOkKrzlWaKXtZxxwW/giphy-downsized-large.gif">

**App.tsx**
```ts
import React from 'react'
import { CompositionLayout } from './Composition/CompositionLayout'
import { Composition } from './Composition'

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

```
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.

<img width="100%" style="width:100%" src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExanFpOWZ3ZjUyMXppNXBxbzlvbGcxMnhudHNmZGE2NDkzem9wa3hjdiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/osjgQPWRx3cac/giphy.gif">