Learn React composition in 15 minutes
Denys
Posted on May 30, 2024
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.
Posted on May 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.