Reusability - React
Notes from Scrimba Advance React course
React allows to create components that can be re-used anywhere its needed in the application. This helps simplify codebase in larger code-bases. The listed topics include:
Children
Compound Components
Context
Composition
Render props
Custom Hooks
Children in HTML
<button>Click Here</button>
<!-- or -->
<select>
<option>Choose Colors</option>
<option>Blue</option>
<option>Yellow</option>
<option>Red</option>
<option>Orange</option>
</select>
React Children
export default function Button(props) {
return (
<button>{props.children}</button>
)
}
//App.jsx
function App() {
return (
<main>
<Button>Buy Now!</Button>
</main>
)
}
More Complex React Children
The concept of adding children to custom components can get complex but below is a simple example of it.
Adding an icon along with the button text. The tedious way is using a library, passing an SVG or using React element with location .
function App() {
return (
<main>
<Button icon="react-element/svg/icon-location">Buy now!</Button>
</main>
)
}
The other way using React children is; you can add to either right or left side
import Button from "./Button"
import { FaMoneyBill } from "react-icons/fa"
function App() {
return (
<main>
<Button>
Buy now!
<FaMoneyBill />
</Button>
</main>
)
}
The CSS on the button
button > svg {
margin-right: 7px;
color: green;
height: 20px;
width: 20px;
}
You can get React Icons from - https://react-icons.github.io/react-icons/
props spreading
If the code has a lot of props to pass to the component, prop spreading syntax can be used to brief the code.
export default function Button(props) {
return (
<button
onClick={props.onClick}
onDoubleClick={props.onDoubleClick}
style={props.style}
className={props.className}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
{/*... etc. ...*/}
{props.children}
</button>
)
}
//The shortened code can be like this
export default function Button(props) {
return (
<button {...props}>
{props.children}
</button>
)
}
Please note the component will only be passed handlers that are recognized by the native element
function App() {
return (
<main>
<Button style={{color: "green"}} onClick={() => console.log("Logging in...")}>
<FcGoogle />
Log in with Google
</Button>
</main>
)
}
Destructuring props
The properties passed to the Button component are the same as above. Here rest
is a variable name and can be named anything for the remainder of properties passed.
export default function Button({children, ...rest}) {
console.log(rest)
return (
<button {...rest}>
{children}
</button>
)
}
Combining Classes
If we want to add more than 1 className to the custom component, we may have to use 3rd party library https://www.npmjs.com/package/classnames or https://www.npmjs.com/package/clsx or otherwise only 1 className is applied whichever comes last.
Without using library:
export default function Button({children, size, ...rest}) {
let sizeClass
if (size === "sm") sizeClass = "button-small"
if (size === "lg") sizeClass = "button-large"
return (
//only size gets applied
<button {...rest} className={sizeClass}>
{children}
</button>
)
}
One Way
export default function Button({children, className, size, ...rest}) {
let sizeClass
if (size === "sm") sizeClass = "button-small"
if (size === "lg") sizeClass = "button-large"
return (
<button className={`${sizeClass} ${className}`} {...rest}>
{children}
</button>
)
}
More professional way using classnames library:
import classnames from "classnames"
export default function Button({children, className, size, ...rest}) {
let sizeClass = size ? `button-${size}` : ""
const allClasses = classnames(sizeClass, className)
return (
<button className={allClasses} {...rest}>
{children}
</button>
)
}
Overloaded Avatar Component
Overloaded components are not best practices for coding React apps in day to day life.The below is an example of how the same component is used to render for 3 different scenarios.
export default function Avatar({src, alt, children}) {
if (src) {
return (
<div className="avatar">
<img src={src} alt={alt}/>
</div>
)
}
if (children) {
return (
<div className="avatar avatar-letters">
{children}
</div>
)
}
else {
return (
<div className="avatar avatar-icon">
<IoPersonSharp />
</div>
)
}
}
//App.jsx
function App() {
return (
<>
<Avatar src="./images/bob.jpg" alt="Bob Ziroll" />
<br />
<Avatar>BZ</Avatar>
<br />
<Avatar />
</>
)
}
Menu Component
The code below explains the Menu component where everything is managed using props. This is how we as beginners in React will approach solving a problem with React.
function App() {
return (
<>
<Menu
buttonText="Sports"
items={["Tennis", "Racquetball", "Pickleball", "Squash"]}
/>
</>
)
}
//Menu
export default function Menu({ buttonText = "Menu", items }) {
const [open, setOpen] = React.useState(true)
function toggle() {
setOpen(prevOpen => !prevOpen)
}
return (
<div className="menu">
<MenuButton
buttonText={buttonText}
onClick={toggle}
/>
{open && <MenuDropdown items={items} />}
</div>
)
}
//MenuButton
export default function MenuButton({ buttonText, onClick }) {
return (
<Button onClick={onClick}>{buttonText}</Button>
)
}
//Button
//its the same as we built above
//MenuDropdown
export default function MenuDropdown({ items }) {
return (
<div className="menu-dropdown">
{items.map(
item => (
<div
className="menu-item"
key={item}
>
{item}
</div>
)
)}
</div>
)
}
The complications with a code like this can be
Inflexible/tightly coupled
Opaque
Not DRY
Prop drilling (Menu passing the props down to MB and MD)
Solution to above is a set of concepts
Compound Components
React.Children API
Context
Implicit State
Prop Drilling
Prop drilling happens when a component down the component tree needs access to data in a grandparent (or higher) component, and that data is manually passed down to each child component until it finally reaches the component that needs it.
Solutions to prop drilling
Do nothing! (for small codebases)
Compound Components
"Flattens" the structure
Easily pass props to more deeply-nested components
Context
- Access state directly from the components that need it.
Compound Components
Composing components together either to add additional functionality or styling is not new.
HTML Examples 💡
<form>, <input>
<ul>, <li>
<table>, <thead>, <tbody>, <td>, etc
<select>, <option>
The primary things to know about using compound components are:
Use Children props
Have dedicated function/styling
Make the component structure more than transparent
Give more control to the "user" of the component
Initial Code without compound components
Compound Components FAQ
How do you define compound components to a layman?
Components that work together to accomplish a greater objective than might make sense to try and accomplish with a single component alone.
What are some examples of HTML elements that work together to add functionality or styling to each other?
<ul> & <li>, <select> & <option>, <table> & all the other table elements
How can compound components help you avoid having to drill props multiple levels down?
Compound component "flatten" the hierarchy that would otherwise be needed to pass props through. Since the parent component provides the children to render, the parent-most component has direct access to those "grandchild" components, to which it can pass whatever props it needs to pass directly.
Code with Compound Components
Everything in the above code as props is re-defined as children and added as components. This is how the structure is now flattened.
Initial Layout
Flattened Structure with Compound Components
Even with this flattened structure, we are unable to make the toggle and click buttons work. Now how are those props/state passed to the right components? This is where the concept Implicit State comes in, a term coined by React community.
Implicit State - React.Children
utility that provides methods for interacting with a component's direct children elements
React.Children.map()
React.Children.forEach()
Others at React docs
React.cloneElement() - similar to Object.assign()
will be used along with the React.Children.map() to provide required props to the components.
A utility that duplicates a React element and provides a way to inject element additional props to that element.
When used with
React.Children.map()
, it can be used to "augment" the original children with new props
export default function Menu({ children }) {
const [open, setOpen] = React.useState(true)
function toggle() {
setOpen(prevOpen => !prevOpen)
}
return (
<div className="menu">
{React.Children.map(children, (child) => {
return React.cloneElement(child, {
open,
toggle
})
})}
</div>
)
}
These props are then respectively accessed in MenuButton and MenuDropdown in its usual way.
Shortcomings with React.Children
Fragile/Delete
limited in depth
The concept only works with direct children and if grandchildren of the component needs access to state, it will have to be defined in the parent component again. If MenuItem needs access to open
or toggle
states, they will have to be defined in MenuDropdown
component in the same way as defined in Menu
component.
React Context
A way to "teleport" data to the components in the tree that need it without passing props.
Provider component is added to the closest ancestor. The component is provided values the other components may need. Other components access these values using useContext
hook.
createContext() & Context Provider
The context is created outside the component function so that it can be exported and accessed by other components. Then inside, the code is wrapped around Provider, providing the values. The values can vary from func, object or primitives.
import React from "react"
const ThemeContext = React.createContext()
export default function App() {
return (
<ThemeContext.Provider value="light">
<div className="container dark-theme">
<Header />
<Button />
</div>
</ThemeContext.Provider>
)
}
export { ThemeContext }
useContext()
The Provider children can access the values passed to the provider via useContext
hook as shown below
import React from "react"
import { ThemeContext } from "./App"
export default function Header() {
const value = React.useContext(ThemeContext)
return (
<header className={`${value}-theme`}>
<h1>{value === "light" ? "Light" : "Dark"} Theme</h1>
</header>
)
}
State + Context
Both can work together for efficient functioning of the app. The context can pass values to multiple components that need it and state can be responsible for changing the values whenever needed.
//App.js
const ThemeContext = React.createContext()
export default function App() {
const [theme, setTheme] = React.useState("light")
function toggleTheme() {
setTheme(prevTheme => prevTheme === "light" ? "dark" : "light")
}
return (
<ThemeContext.Provider value={{theme, toggleTheme}}>
<div className={`container ${theme}-theme`}>
<Header />
<Button />
</div>
</ThemeContext.Provider>
)
}
export { ThemeContext }
//Button.js
import { ThemeContext } from "./App"
export default function Button() {
const { theme, toggleTheme } = React.useContext(ThemeContext)
return (
<button onClick={toggleTheme} className={`${theme}-theme`}>
Switch Theme
</button>
)
}
//Header.js
import { ThemeContext } from "./App"
export default function Header() {
const { theme } = React.useContext(ThemeContext)
return (
<header className={`${theme}-theme`}>
<h1>{theme === "light" ? "Light" : "Dark"} Theme</h1>
</header>
)
}
Headless Components
These components don't have any styled UI to display: they're purely intended to provide functionality. When one or more components have a same functionality, the components can be wrapped within a headless component to perform the function.
For example, the scrim shows code in 2 different ways of toggling a star. It is very similar to what the menu component above was doing. The Toggle component here is a headless component doing the same function as the Star component
Toggle Compound Components
The main toggle will set the following states to be shared on with the children. This is a headless component and will only share functionality.
Button ( to flip state)
On (what to display when Toggle state is "on")
Off (what to display when Toggle state is "off")
Display (Expose internal state to give more control to user)
Creating Toggle Component
import React from "react"
const ToggleContext = React.createContext()
export default function Toggle({ children }) {
const [on, setOn] = React.useState(false)
function toggle() {
setOn(prevOn => !prevOn)
}
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}
export { ToggleContext }
Creating Toggle Button Component
import React from 'react'
import ToggleContext from "./Toggle"
export default function ToggleButton({children}) {
const {toggle} = React.useContext(ToggleContext)
return (
<div onClick={toggle}>
{children}
</div>
)
}
Creating Toggle On & Toggle Off
import React from 'react'
import ToggleContext from "./Toggle"
export default function ToggleOn({children}) {
const {on} = React.useContext(ToggleContext)
return on ? children : null
}
import React from 'react'
import ToggleContext from "./Toggle"
export default function ToggleOff({children}) {
const {on} = React.useContext(ToggleContext)
return on ? null : children
}
In the current state of application, its still using state from the star component. Now that we have on and off state ready, star component can now be removed.
Remove Star Component
The Star component can now be completely removed, now that we have Toggle Button, On and Off components ready. This works via a concept called event bubbling. The click event is bubbled from child to parent.
import React from 'react';
import ReactDOM from 'react-dom/client';
import Star from "./Star"
import Toggle from "./Toggle/index"
import { BsStar, BsStarFill } from "react-icons/bs"
function App() {
return (
<>
<Toggle>
<Toggle.Button>
<Toggle.On>
<BsStarFill className="star filled" />
</Toggle.On>
<Toggle.Off>
<BsStar className="star" />
</Toggle.Off>
</Toggle.Button>
</Toggle>
</>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
The same headless Toggle component can be used for the menu component previously.
<Toggle>
<Menu>
<Toggle.Button>
<Menu.Button>Menu</Menu.Button>
</Toggle.Button>
<Toggle.On>
<Menu.Dropdown>
<Menu.Item>Home</Menu.Item>
<Menu.Item>About</Menu.Item>
<Menu.Item>Contact</Menu.Item>
<Menu.Item>Blog</Menu.Item>
</Menu.Dropdown>
</Toggle.On>
</Menu>
</Toggle>
Composing new components with Toggle
As we noticed, this is a lot of markup code compared to the functionality we want from the menu or star components. The below code moved the toggle logic with the menu components internally, so the user using the Menu component does not need to touch the generic Toggle at all
<Menu>
<Menu.Button>Menu</Menu.Button>
<Menu.Dropdown>
<Menu.Item>Home</Menu.Item>
<Menu.Item>About</Menu.Item>
<Menu.Item>Contact</Menu.Item>
<Menu.Item>Blog</Menu.Item>
</Menu.Dropdown>
</Menu>
//Menu
return (
<Toggle>
<div className="menu">
{children}
</div>
</Toggle>
)
//MenuButton
return (
<Toggle.Button>
<Button>{children}</Button>
</Toggle.Button>
)
//MenuDropdown
return (
<Toggle.On>
<div className="menu-dropdown">
{children}
</div>
</Toggle.On>
)
OnToggle Event Listener
If there is any response that you need to send to and API or a database, then event listener is added to the Toggle Component. In the diagram below, when a star is clicked, a request is to be sent to a database to save the like.
The request workflow will look as below:
export default function Star({ onChange }) {
return (
<Toggle onToggle={onChange}>
<Toggle.Button>
<Toggle.On>
<BsStarFill className="star filled" />
</Toggle.On>
<Toggle.Off>
<BsStar className="star" />
</Toggle.Off>
</Toggle.Button>
</Toggle>
)
}
//App.js
function App() {
return (
<>
<Star onChange={() => console.log("Star toggled")} />
</>
)
}
//Toggle.js
import React from "react"
const ToggleContext = React.createContext()
export default function Toggle({ children, onToggle }) {
const [on, setOn] = React.useState(false)
function toggle() {
setOn(prevOn => !prevOn)
}
React.useEffect(() => {
onToggle()
}, [on])
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}
export { ToggleContext }
The bug in this code is that the console is logged as soon as the App is executed. The database does not need to be called as
useRef() Hook
Refs are just like state, except changing them doesn't cause a re-render. They're frequently used for manual DOM manipulation. It could be paired with useState and useEffect in the app
const renderCount = React.useRef(/* initial value*/)
console.log(renderCount) // {current:0} an object property current of value 0
React.useEffect(() => {
renderCount.current++
})
Code in action
React Docs explains on how Refs can be used for manipulating DOM elements. https://react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref
For a newbie in React, its advised to not use ref with anything that needs a manual change in the DOM. Refs are beneficial when you dont want the page to be re-rendered.
Fix onToggle bug using refs
const ToggleContext = React.createContext()
export default function Toggle({ children, onToggle }) {
const [on, setOn] = React.useState(false)
const firstRender = React.useRef(true)
function toggle() {
setOn(prevOn => !prevOn)
}
React.useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
onToggle()
}
}, [on])
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}
export { ToggleContext }
Render Props
We know we can pass objects, arrays, and string values as props but can we pass functions?
In the example above, sayName
is called inside Decision component, this function now has access to states defined inside the component. It can access the goingOut
state defined in the component and pass it to the parent
export default function Decision({sayName}) {
const [goingOut, setGoingOut] = React.useState(false)
sayName(goingOut)
function toggleGoingOut() {
setGoingOut(prev => !prev)
}
.....
}
//App.js
function App() {
return (
<div>
<Decision sayName={(goingOut) => {
console.log(goingOut ? "I AM going out" : "I'm staying in")
}} />
</div>
)
}
In React, data and props are passed from parent to child, and not vice versa. This is similar to how when addEventListener
is added to a DOM element and event
variable is passed to the callback function that has all the related data about the element.
In the same way in React, callback functions can be used to access the internal state of the child component and then based on the value, perform the action in the parent component.
render
here is a function that returns jsx and hence the name render props :)
The more common way render props is used in React and the way 3rd party libraries would expect you to use is through children
function App() {
return (
<div>
<Decision>
{(goingOut) => {
return (
<h1>
Am I going out tonight?? {goingOut ?
"Yes!" : "Nope..."}
</h1>
)
}}
</Decision>
</div>
)
}
//Decision.js
return (
<div>
<button onClick={toggleGoingOut}>Change mind</button>
{children(goingOut)}
</div>
)
Custom Hooks
Hooks allow you "hook into" the rendering cycles of React. E.g. useState maintains variables across render cycles and triggers re-renders on change. useRef maintains values across render cycles without causing re-renders.
Custom hooks can be created by combining existing hooks into custom, reusable pieces of logic.
useEffectOnUpdate - Custom Hook
export default function useEffectOnUpdate(effectFunction, deps) {
const firstRender = React.useRef(true)
React.useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
effectFunction()
}
}, deps)
}
This hook can now replace the code in the Toggle component
export default function Toggle({ children, onToggle = () => {}}) {
const [on, setOn] = React.useState(false)
function toggle() {
setOn(prevOn => !prevOn)
}
useEffectOnUpdate(onToggle, [on])
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}
The Headless Toggle component created previously can be simplified using custom hooks.
useToggle - Custom Hook
part 1 > Bring the useState and toggle function from the Toggle component to this hook.
export default function useToggle() {
const [on, setOn] = React.useState(false)
function toggle() {
setOn(prevOn => !prevOn)
}
}
part 2 > useToggle returns 'on' and 'toggle' as an array
export default function useToggle() {
const [on, setOn] = React.useState(false)
function toggle() {
setOn(prevOn => !prevOn)
}
return [on, toggle]
}
//Toggle.js
export default function Toggle({ children, onToggle = () => {}}) {
const [on, toggle] = useToggle()
useEffectOnUpdate(onToggle, [on])
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}
part 3 & 4> Remove all references of Toggle in all 4 Menu components and use the new useToggle hook to create state and toggle function
//Menu.js
export default function Menu({ children, onOpen }) {
const [open, toggleOpen] = useToggle()
return (
<div className="menu">
{children}
</div>
)
}
//MenuButton.js
export default function MenuButton({ children }) {
return (
<Button>{children}</Button>
)
}
//MenuDropdown.js
export default function MenuDropdown({ children }) {
return (
<div className="menu-dropdown">
{children}
</div>
)
}
//MenuItem.js
export default function MenuItem({ children }) {
return (
<div className="menu-item">
{children}
</div>
)
}
After the changes, the menu stays open and click does not work
part 5 > Create MenuContext and export it. What values would be provided?
const MenuContext = React.createContext()
export default function Menu({ children, onOpen }) {
const [open, toggleOpen] = useToggle()
return (
<MenuContext.Provider value={{open, toggleOpen}}>
<div className="menu">
{children}
</div>
</MenuContext.Provider>
)
}
export { MenuContext }
part 6 > Access the needed context values in MenuButton and MenuDropdown components
//MenuButton
export default function MenuButton({ children }) {
const { toggleOpen } = React.useContext(MenuContext)
return (
<Button onClick={toggleOpen}>{children}</Button>
)
}
//MenuDropdown.js
export default function MenuDropdown({ children }) {
const { open } = React.useContext(MenuContext)
return (
<>
{open ? (
<div className="menu-dropdown">
{children}
</div>
) : null
}
</>
)
}
part 7 & 8 > When there is an API to be interacted with on click of the Menu , the custom hook can be used as shown in the scrim below
With this, we are finished with Reusability section.
Subscribe to my newsletter
Read articles from Henna Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Henna Singh
Henna Singh
Driven professional with expertise in problem-solving, relationship building, and event organization, complemented by a strong presence in blogging and public speaking. Currently studying Frontend Development Career Path from Scrimba and working as Program Manager at MongoDB. I am actively looking to switch to a more technical profile and build web solutions. I bring great leadership, program management and customer engineering background, that proves my strong interpersonal, team building and resourceful skills