Skip to main content

Context Form Example

This example follows Atomic Design:

  • AtomInput
  • MoleculeUserForm
  • OrganismUserProfile
  • Context → Stores and updates the state

UserContext

File: context/UserContext.tsx

import {
createContext,
useContext,
useState,
ReactNode,
Dispatch,
SetStateAction,
} from 'react'

export interface User {
name: string
age: number
email: string
}

interface UserContextType {
user: User
setUser: Dispatch<SetStateAction<User>>
}

const UserContext = createContext<UserContextType | undefined>(
undefined
)

export const UserProvider = ({children}: { children: ReactNode }) => {

const [user, setUser] = useState<User>({
name: 'René',
age: 30,
email: 'rene@example.com',
})

return (
<UserContext.Provider value={{ user, setUser}}>
{children}
</UserContext.Provider>
)
}

export const useUser = () => {
const context = useContext(UserContext)

if (!context) {
throw new Error(
'useUser must be used inside UserProvider'
)
}

return context
}

Atom

File: components/atoms/Input/Input.tsx

interface InputProps {
value: string
onChange: (value: string) => void
placeholder?: string
}

const Input = ({value, onChange, placeholder}: InputProps) => (
<input value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
/>
)

export default Input

Molecule

File: components/molecules/UserForm/UserForm.tsx

import Input from '../atoms/Input'

interface UserFormProps {
name: string
age: number
email: string
onNameChange: (name: string) => void
onAgeChange: (age: number) => void
onEmailChange: (email: string) => void
}

const UserForm = ({name, age, email, onNameChange, onAgeChange, onEmailChange }: UserFormProps) => {
return (
<div>
<div>
<label>Name</label>

<Input
value={name}
placeholder="Name"
onChange={onNameChange}
/>
</div>

<div>
<label>Age</label>

<Input
value={String(age)}
placeholder="Age"
onChange={(value) =>
onAgeChange(Number(value))
}
/>
</div>

<div>
<label>Email</label>

<Input
value={email}
placeholder="Email"
onChange={onEmailChange}
/>
</div>
</div>
)
}

export default UserForm

Organism

File: components/organisms/UserProfile/UserProfile.tsx


import UserForm from '../molecules/UserForm'
import { useUser } from '../../context/UserContext'

const UserProfile = () => {
const { user, setUser } = useUser()

const handleNameChange = (name: string) => {
setUser((current) => ({
...current,
name,
}))
}

const handleAgeChange = (age: number) => {
setUser((current) => ({
...current,
age,
}))
}

const handleEmailChange = (email: string) => {
setUser((current) => ({
...current,
email,
}))
}

return (
<div>
<h2>User Profile</h2>

<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Email: {user.email}</p>

<UserForm
name={user.name}
age={user.age}
email={user.email}
onNameChange={handleNameChange}
onAgeChange={handleAgeChange}
onEmailChange={handleEmailChange}
/>
</div>
)
}

export default UserProfile

App

// App.tsx

import { UserProvider } from './context/UserContext'
import UserProfile from './components/organisms/UserProfile/UserProfile'

const App = () => (
<UserProvider>
<UserProfile />
</UserProvider>
)

export default App

Architecture

App
└── UserProvider (Context)
└── UserProfile (Organism)
└── UserForm (Molecule)
└── Input (Atom)

Responsibilities

LayerResponsibility
ContextOwns application state
OrganismContains business logic and orchestration
MoleculeGroups related UI elements
AtomSmallest reusable UI element

Notice that the Atom is completely unaware of React Context, the Molecule is unaware of business rules, and all state orchestration lives in the Organism, which keeps the Atomic Design layers clean and reusable.

Assignment

info

Rewrite this code so it only passes a user object to the UserForm component, and create a function updateUser in the UserProfile that updates the user state based on the field name and value.