I am fully convinced that if you are reading this you are definitely not a toddler who writes React, perhaps you are reading this because you have hit "the wall" and seek an escape, the kind of relief that simple and precise information can give. I am no expert but I have hit this wall countless times and that's why I put together this information as simple as I can, just so the future me that hits it again will have a reference to freshen up, and perhaps you can too.
Straight to the chase:
IMO, React sought to solve one major problem, to be a great option as the View of the "MVC" architecture, a problem that I haven't necessarily experienced because I don't know what scope "large applications" fall into, is it 5000+ lines of code, youtube level codebases? perhaps... but what I do know is when you are building very complex web applications with several parts that are likely to be reused repeatedly, you start to wish that you didn't have to copy and paste code so many times (that's what I did at one internship). You begin to wish that you wrote something once and it could be reused many times, that's what components are for. React solves that problem and more...
Some parts of a website are going to hold static data, data that won't change (if so you probably could get away with any good CMS + some templates), and others will continuously display different information depending on what "kind of data" is assigned. This is where state management comes in.
The React core provides two known ways of representing states in a React component, the previously discussed useState hook and the famous "Context API". useState hook and the famous "Context API".
useState and the Context API
Class components in React have state (state is simply the current dynamic value of the component) built-in,
// Example stateful class component
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; // state
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
functional components used to be used just for presentation before the release of hooks made it possible to add state.
That's simple enough, right? What if you need to track several values at once whilst passing them to other components that require the same data to be relevant, that's a common use case.
For example, you have managed to build an amazon eCommerce clone where a user can sign in... you can track the user's login status by using useState and then passing the value through all the child components to whichever component that actually needs the user's email to be displayed, this is known as "prop drilling", and aptly named it is.
// Example stateful function component
function Clock() {
const [dateState, setDate] = React.useState({date: new Date()})
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {dateState.date.toLocaleTimeString()}.</h2>
</div>
);
}
useState is "local" to the functional component and therefore, the only way you will be able to get the data to the child component will be through "prop drilling" which becomes unbearable after about three levels deep. How do you get around this: Most will quickly call on state management tools or perhaps composition, however it appears composition is less used as compared to state management.
To Context or To Compose:
The useContext hook in React solves the problem (prop drilling) easily, but the react docs provide a subtle caution, that it will introduce additional complexity to your code, whilst making your components less reusable ( before you use context ), less reusable in the sense that, once a component is dependent on context to have full functionality, it cannot maintain same functionality outside of context, hence it's less usable, for this reason, they offer an alternative to consider before using context. Outside of the official React docs "Redux Toolkit/Redux" is also very popular as a state manager.
Before Context, consider Composition:
How do you use Composition to get past this issue, the react docs referenced in the previous paragraph highlight how but here's an example with code
Prop Drilling: The user data is passed 4 levels deep into the component hierarchy to reach the component that actually needs it.
// Before implementing context
import React, { useState } from "react";
//* Main Parent Component
function App() {
let [currentUser, setCurrentUser] = useState(null);
return (
<div
className="App"
style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<div style={{ backgroundColor: "lightgray" }}>
<Header />
</div>
<div style={{ flex: 1 }}>
{currentUser ? (
// passing user as prop to Dashboard
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "John Doe" })} />
)}
</div>
<div style={{ backgroundColor: "lightgray" }}>
<Footer />
</div>
</div>
);
}
//* Children Components
function Header() {
return <div>Header</div>;
}
function LoginScreen({ onLogin }) {
return (
<div>
<h3>Please login</h3>
<button onClick={onLogin}>Login</button>
</div>
);
}
function Dashboard({ user }) {
return (
<div>
<h2>The Dashboard</h2>
<DashboardNav />
// Passing user prop to DashboardContent
<DashboardContent user={user} />
</div>
);
}
function DashboardNav() {
return (
<div>
<h3>Dashboard Nav</h3>
</div>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
// Passing user prop to WelcomeMessage
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
// Welcome message finally gets component,
// and this is prop drilling at it's worst.
return <div>Welcome {user.name}</div>;
}
function Footer() {
return <div>Footer</div>;
}
export default App;
After Context:
//* Main Parent Component
// initialising context
let Context = React.createContext();
function App() {
let [currentUser, setCurrentUser] = useState(null);
return (
// Context wraps around the main parent component, any child component within,
// has access to whatever value is in context.
<Context.Provider value={{ currentUser }}>
<div
className="App"
style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<div style={{ backgroundColor: "lightgray" }}>
<Header />
</div>
<div style={{ flex: 1 }}>
{currentUser ? (
<Dashboard />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "John Doe" })} />
)}
</div>
<div style={{ backgroundColor: "lightgray" }}>
<Footer />
</div>
</div>
</Context.Provider>
);
}
//* Children Components
function Header() {
return <div>Header</div>;
}
function LoginScreen({ onLogin }) {
return (
<div>
<h3>Please login</h3>
<button onClick={onLogin}>Login</button>
</div>
);
}
function Dashboard() {
return (
<div>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent />
</div>
);
}
function DashboardContent() {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage />
</div>
);
}
// Notice that there is no prop drilling...
// And the component that needs the prop is the one that gets it...
// However, this component's reuse is now dependent on context...
function WelcomeMessage() {
let { currentUser } = React.useContext(Context);
return <div>Welcome {currentUser.name}</div>;
}
function DashboardNav() {
return (
<div>
<h3>Dashboard Nav</h3>
</div>
);
}
function Footer() {
return <div>Footer</div>;
}
export default App;
Now to Composition
Now that we have explored solving the problem with Context, let's take a look at using Composition to solve the same problem. Composition aims to maintain the reusability of the component whilst avoiding prop drilling.
We will do this by making use of the children prop that's available to us in React. The children prop allows you to create "wrapper components", these components wrap take a component or components and render them/it. Observe the basic example below to understand the final implementation.
function ComponentA ({children}) {
return(
{children}
)
}
// the wrapper component
<ComponentA>
// the child component
<childofA/>
</ComponentA>
I hope this brief demo is OK for now, otherwise here's some expansion on the topic Composition and Inheritance
Now to the much-awaited solution:
//* Main Parent Component
// Notice that we are no more using the context hook
function App() {
let [currentUser, setCurrentUser] = useState(null);
return (
<div
className="App"
style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<div style={{ backgroundColor: "lightgray" }}>
<Header />
</div>
<div style={{ flex: 1 }}>
{currentUser ? (
<Dashboard> // wrapper component
<DashboardNav /> // wrapper component
<DashboardContent> // wrapper component
// and we pass our prop, whilst escaping drilling, atleast three times.
<WelcomeMessage user={currentUser} />
</DashboardContent>
</Dashboard>
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "John Doe" })} />
)}
</div>
<div style={{ backgroundColor: "lightgray" }}>
<Footer />
</div>
</div>
);
}
Additional Useful Implementation
How to create Wrapper components
Composition, alternative to prop drilling
When and How to use Composition