24
loading...
This website collects cookies to deliver better user experience
function Card({children}){
return (
<div className="Card">
{children}
</div>
);
}
export default Card;
<Card>
// Content goes here
</Card>
function Card({children}){
...
}
function Heading({children}){
return (
<h2 className="Card__heading">
{children}
</h2>
);
}
export Heading;
export default Card;
<Card>
<Heading>My title</Heading>
</Card>
<Heading>My title</Heading>
<Card>
// Oh no, I want my Heading to only be in here!
</Card>
function Card({children}){
...
}
function Heading({children}){
...
}
Card.Heading = Heading;
export default Card;
<Card>
<Card.Heading>My title</Card.Heading>
</Card>
<Card.Heading>My title</Card.Heading>
<Card>
// Oh no, I want my Heading to only be in here!
</Card>
import { createContext } from "react";
var CardContext = createContext();
function Card({children}){
...
}
function Heading({children}){
...
...
import { createContext } from "react";
var CardContext = createContext();
function Card({children}){
return (
<CardContext.Provider value={{}}>
<div className="Card">
{children}
</div>
</CardContext.Provider>
);
}
function Heading({children}){
...
...
value={// whatever you want}
which is then available to all nested children.import { createContext, useContext } from "react";
...
function Heading({children}){
var context = useContext(CardContext);
return (
<h2 className="Card__heading">
{children}
</h2>
);
}
context
variable holds whatever value we define in the value prop of the provider value={// whatever you want}
, in our case this is just an empty object value={{}}
.<Card.Heading>
outside <Card>
(which is the provider), the context
variable inside <Card.Heading>
would be undefined
, while if rendered inside, would contain the empty object {}
....
function Heading({children}){
var context = useContext(CardContext);
if (!context) {
return (
<p className="Card__scopeError>
I want to be inside the Card component!
</p>
)
}
return (
<h2 className="Card__heading">
{children}
</h2>
);
}
<Card.Heading>
outside <Card>
, a p-tag with our "error message" is rendered instead of our h2 which forces us to only use it inside <Card>
. Great!return
statement inside <Card.Heading>
can be boiled down to a single line using a custom hook which makes it a lot cleaner and easier to create new child components.function useObjectState(initialValue){
var [state, setState] = useState(initialValue);
return {state, setState};
}
function objectState(initialValue){
var [state, setState] = useState(initialValue);
return {state, setState};
}
React Hook "useState" is called in function "objectState" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter react-hooks/rules-of-hooks
import { createContext, useContext } from "react";
...
function useCardContext(){
var context = useContext(CardContext);
if (!context) {
throw new Error("Child components of Card cannot be rendered outside the Card component!");
}
return context;
}
function Card({children}){
...
...
function useCardContext(){
...
}
function Heading({children}){
var context = useCardContext();
return (
<h2 className="Card__heading">
{children}
</h2>
);
}
...
...
function Heading({children}){
var context = useCardContext();
...
}
function Button({children}){
var context = useCardContext();
return (
<button className="Card__button">
{children}
</button>
);
}
Card.Button = Button;
...
<Card>
<Card.Heading>My title</Card.Heading>
<Card.Button>Toggle</Card.Button>
</Card>
<Card>
and then share that state with the other child components through the context. Since we have already created our context, the sharing is just super easy, so let's add that state and the context value (provider value):import { createContext, useContext, useState } from "react";
...
function Card({children}){
var [toggled, setToggled] = useState(false);
return (
<CardContext.Provider value={{toggled, setToggled}}>
...
</CardContext.Provider>
);
}
...
<Card>
) and added toggled
and setToggled
to the value prop of its provider (<CardContext.Provider value={{toggled, setToggled}}>
).toggled
and setToggled
as properties and passed that object in as the value for the provider? I want to be able to only "grab" the values I need inside the child components, for example in <Card.Button>
we need setToggled
to toggle the state in our onClick event, so we just "grab" setToggled
from the context:...
function Button({children}){
var {setToggled} = useCardContext();
return (
<button
className="Card__button"
onClick={() => setToggled(prev => !prev)}
>
{children}
</button>
);
}
Card.Button = Button;
...
var {setToggled} = useCardContext();
.var [toggled, setToggled] = useCardContext();
, which would have left toggled
as an unused variable.context
variable from before, but be aware of the dot syntax you would then have to use (onClick={() => context.setToggled(prev => !prev)}
).<Card>
we just use the defined toggled
state to toggle a CSS class on the div
:...
function Card({children}){
var [toggled, setToggled] = useState(false);
return (
<CardContext.Provider value={{toggled, setToggled}}>
<div className={toggled ? "Card Card--highlight" : "Card"}>
{children}
</div>
</CardContext.Provider>
);
}
...
toggled
from the context:...
function Heading({children}){
var {toggled} = useCardContext();
return (
<h2 className={
toggled
? "Card__heading Card__heading--highlight"
: "Card__heading"}
>
{children}
</h2>
);
}
...
There is state that lives inside of this system.
It's not application state. This isn't stuff that we wanna put over in Redux.
It's not component state because my component has its own state here.
This is its own little system, its own little world of components that has some state that we need to shuffle around.
<Card.Heading>
and <Card.Button>
) only holds a single html (jsx) element? This is one of the things that makes the compound component pattern so very powerful, because now your <Card>
component just became very flexible, for example you can do this if you want:<Card>
// Who says the button should'nt be above the title?
// Well you do...! You decide where it should go.
<Card.Button>Toggle</Card.Button>
<Card.Heading>My title</Card.Heading>
</Card>
div
's (or other element types) that each need some attribute.<Card title="My title" button={true} />
className
's? Should we add a prop to place the button above? Something like this:<Card
style={{border: "2px solid blue"}}
className="MyCard"
title="My title"
titleClass="MyTitle"
titleStyle={{color: "blue"}}
button={true}
buttonAbove={true}
buttonClass="MyButton"
buttonStyle={{border: "1px dotted blue"}}
/>
className
, etc.? A gigantic amount of props and sooo many if statements... No thanks!...
function Image({src, alt, type}){
useCardContext();
return (
<img
className={`Card__image${type
? " Card__image--" + type
: ""}`}
src={src}
alt={alt}
/>
);
}
Card.Image = Image;
...
<Card>
<Card.Heading>My title</Card.Heading>
<Card.Image
src="/path/to/image.jpg"
alt="Our trip to the beach"
/>
<Card.Button>Toggle</Card.Button>
</Card>
<Card>
<Card.Image
src="/path/to/avatar-image.jpg"
alt="This is me"
type="avatar"
/>
<Card.Heading>My title</Card.Heading>
<Card.Button>Toggle</Card.Button>
</Card>
Card__image--avatar
and any other type
you pass in....
function Card({children}){
var [toggled, setToggled] = useState(false);
var [something, setSomething] = useState(null);
return (
<CardContext.Provider
value={{
toggled,
setToggled,
something,
setSomething
}}
>
...
</CardContext.Provider>
);
}
...
Card.js
and Card.css
and paste the following code into each file respectively:Card.js
:import { createContext, useContext, useState } from "react";
import "./Card.css";
// Context (Scope)
var CardContext = createContext();
function useCardContext(){
var context = useContext(CardContext);
if (!context) {
throw new Error("Child components of Card cannot be rendered outside the Card component!");
}
return context;
}
// Card component (main/parent component)
function Card({children}){
var [toggled, setToggled] = useState(false);
return (
<CardContext.Provider value={{toggled, setToggled}}>
<div className={toggled ? "Card Card--highlight" : "Card"}>
{children}
</div>
</CardContext.Provider>
);
}
// Heading component (sub component)
function Heading({children}){
var {toggled} = useCardContext();
return (
<h2 className={
toggled
? "Card__heading Card__heading--highlight"
: "Card__heading"}
>
{children}
</h2>
);
}
Card.Heading = Heading;
// Button component (sub component)
function Button({children}){
var {setToggled} = useCardContext();
return (
<button
className="Card__button"
onClick={() => setToggled(prev => !prev)}
>
{children}
</button>
);
}
Card.Button = Button;
// Image component (sub component)
function Image({src, alt, type}){
useCardContext();
return (
<img
className={`Card__image${type
? " Card__image--" + type
: ""}`}
src={src}
alt={alt}
/>
);
}
Card.Image = Image;
export default Card;
Card.css
:/* Card */
.Card{
border: 1px solid lightgray;
}
.Card--highlight{
border-color: hotpink;
}
/* Heading */
.Card__heading{
margin: 20px;
}
.Card__heading--highlight{
color: hotpink;
}
/* Button */
.Card__button{
border: none;
background-color: hotpink;
padding: 10px 20px;
margin: 20px;
}
/* Image */
.Card__image{
width: 100%;
}
.Card__image--avatar{
width: 48px;
height: 48px;
border-radius: 50%;
margin: 13px 20px 0;
float: left;
}
Card
component where you need it, for example in App.js
:// Remember to update the path to point to the
// correct location of your Card component:
import Card from "./components/Card";
import "./App.css"
function App(){
return (
<div className="App">
{/* First example from the tutorial */}
<Card>
<Card.Heading>My title</Card.Heading>
<Card.Button>Toggle</Card.Button>
</Card>
{/* Example with button and heading flipped */}
<Card>
<Card.Button>Toggle</Card.Button>
<Card.Heading>My title</Card.Heading>
</Card>
{/* Example with image */}
<Card>
<Card.Heading>My title</Card.Heading>
<Card.Image
src="https://picsum.photos/300/100?random=0"
alt="Our trip to the beach"
/>
<Card.Button>Toggle</Card.Button>
</Card>
{/* Example with an avatar-image (type="avatar") */}
<Card>
<Card.Image
src="https://picsum.photos/48?random=1"
alt="This is me"
type="avatar"
/>
<Card.Heading>My title</Card.Heading>
<Card.Button>Toggle</Card.Button>
</Card>
</div>
);
}
export default App;