[LS] Advanced React Concepts
Before jumping into the actual content, this post (and the upcoming ones) is one of the cheat sheets I keep in my Notion account. Since these notes were intended for personal use, the writing style is such that it is easy for me to understand. I want to share them online in case someone finds them useful.
Let's say you wanted a quick view of some advanced React concepts and how to use them.
Disclaimer: These notes were made following the Epic React course by Kent C. Dodds. You can find the code shown in this post on his GitHub page. If you want to deeply understand React and become a better engineer, I highly recommend purchasing this course.
Hook Flow
A great example you can run locally to see how this works is by running this hook-flow.js file.
JSX
JSX is a simple HTML syntactic sugar on top of React’s raw API (e.g. React.createElement
), which is easier to understand. JSX is not Javascript, so to run it, it needs to be converted using a code compiler such as Babel. You can use interpolation to utilise JS-specific functionality (e.g. {myFunc()}
).
Dependency Arrays
The useEffect
dependency array makes a swallow comparison between values. If you put an object in the list, it's always considered a new value and the effect is being rerun. Keep in mind that React makes use of something like ===
or Object.is
underneath to compare previous and current values.
Lazy State Initializer
When we pass a default value to useState
that is not a primitive value and maybe needs to make an async call to retrieve it (e.g. reading from localStorage
) then we need to pass a function to useState
to prevent unnecessary re-renders. This function could also be an inline arrow function instead of creating the function itself.
// Correct
const [name, setName] = React.useState(() ⇒ window.localStorage.getItem(’name’) || ‘John Doe’)
// Correct
function getName() {
return window.localStorage.getItem(’name’) || ‘John Doe’
}
const [name, setName] = React.useState(getName)
// Wrong
const [name, setName] = React.useState(window.localStorage.getItem(’name’) || ‘John Doe’)
Hook: useLayoutEffect
This hook ensures that the function passed to it is called as soon as the component is mounted without waiting for the browser to paint it on the screen. This hook is also called before any other effect (e.g. useEffect
). To bring it down to a rule, you should use useEffect
almost all the time and useLayoutEffect
if the side effect that you are performing makes an observable change to the DOM that will require the browser to paint that update that you've made.
Hook: useContext
Let's say we have a CountProvider
context and a useCount
hook that returns getters/setters for a count value. You can use the following approach for better console errors when useCount
is used outside of a CountProvider
.
// File: {..}/context/count-context
const CountContext = React.createContext()
function CountProvider(props) {
const [count, setCount] = React.useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {…props} />
}
function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within the CountProvider`)
}
return context
}
Hook: useDebugValue
This is useful only for your custom hooks. You can't use this directly inside components. The React DevTools browser extension uses this.
Context Module Functions
Meta, React DevTools use this concept a lot.
In a nutshell, the idea is to have a function that can be exported and used in conjunction with the dispatch
function that your hook exposes. This function accepts the dispatch
function (along with other arguments) and calls this dispatch
function appropriately.
Let's say you have a context/counter-context.js
module that exports a CounterProvider
, a useCounter
and a updateCounter
hook. The useCounter
hook exposes [state, dispatch]
. In order to update the state of the counter, you have to pass the dispatch
function to the updateCounter
along with any other arguments needed by the updateCounter
function.
This may look overkill, and it is. However, this pattern helps to reduce duplication (no need to create a ton of helper functions that need to be memoised), helps improve performance and helps avoid mistakes in the dependency lists. I wouldn’t recommend this all the time, but sometimes it can be helpful.
An example of how this pattern works in code can be found here.
Compound Components
Compound components work together to form a complete UI (e.g. select → option elements). ReachUI heavily uses this pattern.
Simple usage
const ToggleOn = ({on, children}) ⇒ (on ? children : null)
const ToggleOff = ({on, children}) ⇒ (on ? null : children)
const ToggleButton = ({on, toggle}) ⇒ <Switch on={on} onClick={toggle} />
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () ⇒ setOn(!on)
return React.Children.map(children, child ⇒ {
if (typeof child.type === ‘string’) {
return child
}
const newChild = React.cloneElement(child, {on, toggle})
return newChild
}
}
// Component usage
<Toggle>
<ToggleOn>On</ToggleOn>
<ToggleOff>Off</ToggleOff>
<span>Demo</span>
<ToggleButton />
</Toggle>
Flexible Compound Components with Context
With this approach, we don’t care about the depth of the tree (HTML elements). You can use a context where we can share this implicit state and expose it to people without having them worry about the state that's being managed to make these things work together.
const ToggleContext = React.createContext()
ToggleContext.displayName = 'ToggleContext'
function Toggle({onToggle, children}) {
const [on, setOn] = React.useState(false)
const toggle = () ⇒ setOn(!on)
const value = {on, toggle}
return <ToggleContext.Provider value={value}>{children}</ToggleContext.Provider>
}
function useToggle() {
const context = React.useContext(ToggleContext)
if (!context) {
throw new Error(`useToggle must be used within the ToggleProvider`)
}
return context
}
function ToggleOn({children}) {
const {on} = useToggle()
return on ? children : null
}
function ToggleOff({children}) {
const {on} = useToggle()
return on ? null : children
}
const ToggleButton(props) {
const {on, toggle} = useToggle()
return <Switch on={on} onClick={toggle} {...props} />
}
// Component usage
<Toggle>
<ToggleOn>On</ToggleOn>
<ToggleOff>Off</ToggleOff>
<span>Demo</span>
<div>
<ToggleButton />
</div>
</Toggle>
Instead of returning a React.Children.map of all the children and forwarding along the props by making clones of those children, we created a context, and then we rendered the provider with the value of those things that we wanted to provide to those children. In each of the children, we consumed that context so we can have access to that implicit state. There are use cases where you want to enforce the fact that it only makes sense to have children as direct descendants of the parent (e.g. select→option) and you don't like to share things through context because that relationship is an important part of the API. There are use cases for both of these methods, but in general, you can use the context approach always.
Prop Collection and Getters
Prop Collection
function useToggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return {
on,
toggle,
togglerProps: {
'aria-pressed': on,
onClick: toggle
}
}
}
function App() {
const {on, togglerProps} = useToggle()
return (
<div>
<Switch on={on} {...togglerProps} />
<button aria-label="custom-button" {...togglerProps}>
{on ? 'On' : 'Off'}
</button>
</div>
)
}
It probably looks too simple, but this pattern is very important. It is important that we continue to pass the on
state and the toggle
function here so that people who are building UIs can continue to build those UIs accessing that state and the mechanism for updating the state and they don't mesh up with the toggler props that we're providing. In this case, we get the toggler props, we spread them across the position of the props of our switch, and we do the same for the props. Now, we have an accessible and working switch and toggle button.
Prop Getters
This is probably a better pattern than collections because it’s more flexible.
function useToggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
function getTogglerProps({onClick, ...props}) {
return {
'aria-pressed': on,
onClick: () => {
onClick && onClick()
toggle()
},
...props,
}
}
}
function App() {
const {on, getTogglerProps} = useToggle()
return (
<div>
<Switch {...getTogglerProps({on})} />
<button aria-label="custom-button" {...togglerProps}>
{on ? 'On' : 'Off'}
</button>
</div>
)
}
//////// Fancy way
// This will take any number of functions, and then it will return a function that takes any number of arguments. I don't care what those arguments are.
// We'll take those functions. For each of those, we'll take that function.
// What it allows me to do here now is I can say, "Give me a function that will call all of the functions I pass you, onClick and toggle."
// It'll call onClick, and then it'll call toggle. It'll only call onClick if that function actually exists.
function callAll(...fns) {
return (...args) => {
fns.forEach(fn => {
fn && fn(...args)
})
}
}
function useToggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
function getTogglerProps({onClick, ...props}) {
return {
'aria-pressed': on,
onClick: callAll(onClick, toggle),
...props,
}
}
}
Subscribe to my newsletter
Read articles from Giorgos Athanasoulias directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Giorgos Athanasoulias
Giorgos Athanasoulias
If you are interested to learn more about me you can visit my personal website, https://athanasu.com/