Show More component
🌱 This post is in the growth phase. It may still be useful as it grows up.
This totorial is a complete guide to basic react hooks.
It demonstratens simple implementations of useState, useEffect, useRef, useContext, and useId in a familiar, real-world component.
Contents
- Online sandboxes
- Create a component that renders children and a “Show less” button
- Conditionally render toggle text using a ternary operator
- Manage
expandedstate with state with theReact.useStatehook - Add an
onClickevent handler to the button - Call
React.useState’s update function with new state - Style the content container with a
styleprop - Conditionally style content container based on state
- Access content container’s
scrollHeightwithReact.useRef - Set
scrollHeightof DOM node on button click - Set
maxHeightof content container withcontentHeightstate value - Use
React.useEffectto setcontentHeight - Hide button if below height threshhold
- Update when window resizes, with
useEffect - Spot the memory leak with
console.log - Refactor the resize
useEffectto call a function by reference - Add
useEffectcleanup function - Debounce resize event handler
- Extract custom
useEffecthook - Create Context with
React.createContext - Provide Context with the
Context.Providercomponent - Consume Context with
useContext - Provide value and set function on Context
- Repeat for any required Contexts
Online sandboxes
- CodeSandbox Used by me (Vim support)
- StackBlitz
Make your own
Copy-paste this into your prefered React environment.
Does not include bootstrapping with React.creatRoot.
/* START HERE */function ShowMore(/* props */) { return <div>🫵 YOUR IMPLEMENTATION 🫵</div>}
export default function App() { return ( <ShowMore> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu purus turpis. Nulla efficitur pulvinar dui id imperdiet. Nulla cursus nulla id elit imperdiet commodo. Praesent ullamcorper eros quis maximus varius. Integer pellentesque urna nulla, nec vestibulum leo malesuada quis. Maecenas sit amet mauris eu diam blandit molestie bibendum sit amet mauris. Nullam sed posuere lacus. Sed cursus bibendum turpis tincidunt volutpat. Duis molestie volutpat urna, in rutrum ante rhoncus volutpat. Maecenas a imperdiet dolor. Duis ut ex tincidunt, tincidunt velit in, vehicula dolor. Suspendisse dictum porttitor massa. Cras pulvinar ultricies lacus ut maximus. In gravida turpis purus, eu mattis odio tincidunt eget. </ShowMore> )}Create a component that renders children and a “Show less” button
Let’s start.
Create a Component that renders children and a “Show less” button
(If you’re sure what this code does, visit my React Basics tutorial for a primer.)
// 1. Create a component that destructures `children` from propsfunction ShowMore({children}) { return ( <div> {/* 2. Render the `children` */} <div>{children}</div> {/* 3. Render a "Show less" button */} <button>Show less</button> </div> )}Reference: React Basics, chan.dev.
Conditionally render toggle text using a ternary operator
The “Show more” button should read “Show less” when expanded. Add a condition around the button text so that it can be toggled.
(Don’t worry about state. Just activate it, manually, with true and false.)
function ShowMore({ children }) { return ( <div> <div>{children}</div> {/* Conditionally render toggle text using a ternary operator. */} <button>Show less{true ? "less" : "more"}</button> </div> );}Reference: Conditional (ternary) Operator, MDN.
Manage expanded state with state with the React.useState hook
The React.useState hook is how we manage UI state in React.
Create a state, using a initial value. Then assign that state to a local variable by destructuring the returned array.
function ShowMore({ children }) { // 1. Track initial state using and provide an initial value // 2. Assign it to a local variable by destructuring the returned array let [expanded] = React.useState(true);
return ( <div> <div>{children}</div> <button>Show {expanded ? "less" : "more"}</button> </div> );}Reference:
- Destructuring Assignment, MDN.
- useState hook reference, react.dev.
Add an onClick event handler to the button
UI changes are activated by user input.
Add an onClick event handler to the button and log the expanded state.
function ShowMore({ children }) { let [expanded] = React.useState(true);
return ( <div> <div>{children}</div> <button onClick={() => console.log(expanded)}> Show {expanded ? "less" : "more"} </button> </div> );}Reference: Responding to Events, React Docs.
Call React.useState’s update function with new state
The second value that we get from React.useState is a state set function.
Assign that function to a local variable by destructuring it from the returned array. Then call the set function in the button’s onClick event handler.
(To toggle the next state, invert the current state with ! operator.)
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true);
return ( <div> <div>{children}</div> <button onClick={() => console.log(expanded)}> <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> </div> );}Reference: useState set functions referenc, react.dev
Style the content container with a style prop
Styles rules can be applied directly to elements using the style prop.
Style the collapsed state of the ShowMore component to be 100px tall and hide overflowing content.
(We want to animate this. So use maxHeight and set a transition.)
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true);
return ( <div> <div style={{ maxHeight: "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> </div> );}Reference: Applying CSS style, react.dev.
Conditionally style content container based on state
Style rules can be set conditionally using the ternary operator.
Use a ternary to switch the maxHeight value from 100px to "none" when expanded.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true);
return ( <div> <div style={{ maxHeight: expanded ? "none" : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> </div> );}Access content container’s scrollHeight with React.useRef
React.useRef is used to access the DOM node rendered from a component.
Create a ref object. Then pass it to an React element via the ref (special) prop.
Finally, log out the value in the onClick button handler to verify.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true);
const contentRef = React.useRef(null);
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? "none" : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => { setExpanded(!expanded); console.log(contentRef.current.scrollHeight); }} > Show {expanded ? "less" : "more"} </button> </div> );}Reference: useRef hook reference, react.dev.
Set scrollHeight of DOM node on button click
Any state that we need to make rendering decisious should be tracked in state.
Set the scrollHeight of our DOM node, on state, when our button is clicked.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? "none" : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => { setExpanded(!expanded); setContentHeight(contentRef.current.scrollHeight); }} > Show {expanded ? "less" : "more"} </button> </div> );}Set maxHeight of content container with contentHeight state value
If we want to see our transition, we need to explicitly set the maxHeight of the content container (CSS stuff 😭).
Use the contentHeight value (instead of "none") to set the expanded container maxHeight.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => { setExpanded(!expanded); setContentHeight(contentRef.current.scrollHeight); }} > Show {expanded ? "less" : "more"} </button> </div> );}Reference: Using CSS Transitions on Auto Dimensions, CSS Tricks.
Use React.useEffect to set contentHeight
Setting contentHeight on click is has a major flaw.
The first click will not saw the transition animation.
Move the setContentHeight function into a React.useEffect to set the contentHeight on every render.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { if (contentRef.current) { setContentHeight(contentRef.current.scrollHeight); } });
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> <button onClick={() => { setExpanded(!expanded); setContentHeight(contentRef.current.scrollHeight); }> Show {expanded ? "less" : "more"} </button> </div> );}References: useEffect hook reference, react.dev.
Hide button if below height threshhold
This component has no purpose if content height is under our 100px threshold.
Hide the the “Show more” button if height is less than 100px.
Use the Logical AND operator (&&).
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { if (contentRef.current) { setContentHeight(contentRef.current.scrollHeight); } });
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> {contentHeight > 100 && ( <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> )} </div> );}Reference: Logical AND operator (&&), react.dev.
Update when window resizes, with useEffect
Use effect is design to sync events that external to React.
Add a new useEffect that listens for window resize events and updates contentHeight.
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { if (contentRef.current) { setContentHeight(contentRef.current.scrollHeight); } });
React.useEffect(() => { window.addEventListener("resize", () => { setContentHeight(contentRef.current.scrollHeight); }); });
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> {contentHeight > 100 && ( <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> )} </div> );}Spot the memory leak with console.log
Not everything can be observed with console.log.
But useEffect often can.
Add a console.log statement to our resize useEffect to see how often it’s called.
React.useEffect(() => { window.addEventListener("resize", () => { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); });});(It’s also easy to see this in the profiler tab of standard Chrome DevTools.)
Refactor the resize useEffect to call a function by reference
Writing functions inline is extremely convenient.
But it means that we’re creating a new function every render.
Separate the event handler declaration function and the addEventLister call.
function handleResize() { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); }
React.useEffect(() => { window.addEventListener("resize", () => { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); }); window.addEventListener("resize", handleResize);});Add useEffect cleanup function
useEffect callback functions can return a cleanup function to unmount event listers and prevent memory leaks.
Return a function from the resize useEffect to remove the event listener.
React.useEffect(() => { window.addEventListener("resize", handleResize);
return () => { window.removeEventListener("resize", handleResize); };});Reference: Thinking from the Effect’s perspective, react.dev.
Debounce resize event handler
Debouncing is a technique to prevent a function from being called too frequently.
Wrap the handleResize function in the debounce utility.
function handleResize() {let handleResize = debounce(function () { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…");}});This requires a debounce function.
I’d recommend using this one from lodash. In our case, you can use this one:
function debounce(func, timeout = 300) { let timer return (...args) => { clearTimeout(timer) timer = setTimeout(() => { func.apply(this, args) }, timeout) }}Extract custom useEffect hook
We’re not limited to the hooks included in React.
Extract the resize useEffect into a custom hook function. Take handleResize as an argument.
function useWindowResize(handleResize) { React.useEffect(() => { window.addEventListener("resize", handleResize);
return () => { window.removeEventListener("resize", handleResize); }; });}
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { setContentHeight(contentRef.current.scrollHeight); });
let handleResize = debounce(function () { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); });
React.useEffect(() => { window.addEventListener("resize", handleResize);
return () => { window.removeEventListener("resize", handleResize); }; }); useWindowResize(handleResize);
return ( <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> {contentHeight > 100 && ( <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> )} </div> );}Reference: Custom Hooks, react.dev.
Create Context with React.createContext
Context is a way to share state between components without passing props.
Create a new context, to share expanded state with React.createContext.
let ExpandedContext = React.createContext();Reference: createContext hook reference, react.dev.
Provide Context with the Context.Provider component
Context is provided to children with the Context.Provider component.
Wrap the ShowMore render function in the ExpandedContext.Provider component and pass the expanded state to it’s value prop.
function ShowMore({ children }) { /* ...code hidden for brevity... */
return ( <ExpandedContext.Provider value={expanded}> <div>{/* ...code hidden for brevity... */}</div> </ExpandedContext.Provider> );}Reference: Context.Provider component reference, react.dev
Consume Context with useContext
Contexts are consumed in components with the useContext hook.
Create a new component that consumes the ExpandedContext and renders the expanded state.
function ShowMoreButton() { let expanded = React.useContext(ExpandedContext)
return <button>Show {expanded ? 'less' : 'more'}</button>}Reference: useContext hook reference, react.dev.
Provide value and set function on Context
For actions — like buttons — the components need access to state values and set functions.
Modify the Context Provider and useContext consumer to use an array: [expanded, setExpanded].
const ExpandedContext = React.createContext();
function ShowMoreButton() { let expanded = React.useContext(ExpandedContext); let [expanded, setExpanded] = React.useContext(ExpandedContext);
return ( <button> <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> );}
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { setContentHeight(contentRef.current.scrollHeight); });
let handleResize = debounce(function () { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); });
React.useEffect(() => { window.addEventListener("resize", handleResize);
return () => { window.removeEventListener("resize", handleResize); }; });
useWindowResize(handleResize);
return ( <ExpandedContext.Provider value={expanded}> <ExpandedContext.Provider value={[expanded, setExpanded]}> <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> {contentHeight > 100 && <ShowMoreButton />} </div> </ExpandedContext.Provider> );}Repeat for any required Contexts
const ExpandedContext = React.createContext();const ContentHeightContext = React.createContext();
function ShowMoreButton() { let [expanded, setExpanded] = React.useContext(ExpandedContext); let [contentHeight, setContentHeight] = React.useContext(ContentHeightContext);
return ( <> {contentHeight > 100 && ( <button onClick={() => setExpanded(!expanded)}> Show {expanded ? "less" : "more"} </button> )} </> );}
function ShowMore({ children }) { let [expanded, setExpanded] = React.useState(true); let [contentHeight, setContentHeight] = React.useState();
const contentRef = React.useRef(null);
React.useEffect(() => { setContentHeight(contentRef.current.scrollHeight); });
let handleResize = debounce(function () { setContentHeight(contentRef.current.scrollHeight); console.log("resizing…"); });
React.useEffect(() => { window.addEventListener("resize", handleResize);
return () => { window.removeEventListener("resize", handleResize); }; });
useWindowResize(handleResize);
return ( <ExpandedContext.Provider value={[expanded, setExpanded]}> <ContentHeightContext.Provider value={[contentHeight, setContentHeight]}> <div> <div ref={contentRef} style={{ maxHeight: expanded ? contentHeight : "100px", overflow: "hidden", transition: "all .5s ease", }} > {children} </div> {/* {contentHeight > 100 && <ShowMoreButton />} */} <ShowMoreButton /> </div> </ContentHeightContext.Provider> </ExpandedContext.Provider> );}Bonus:
- Put this entire package into a re-usable module and rename accordingly
- Improve accessibility of component