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/**/*"]
}
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>
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 />)
and the App.tsx
as an entry point for our app.
App.tsx
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
//...
"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
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
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 underCompositionContextProvider
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>
}
Footer.tsx
import React from 'react'
import { useCompositionContext } from './Context'
export const Footer = () => {
const context = useCompositionContext()
return <div>Footer</div>
}
Body.tsx
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
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
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
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
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:
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.
App.tsx
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.
Top comments (0)