How to Use useEffect (and other hooks) in Class Components
Hooks are a great new feature in React. The first initial case I found them useful was where we had to create class components just because we wanted to use a single ref or store one variable in state. Now in those cases, we can use hooks to write more succinct code. Here’s a quick example using useState
:
Using useState
Old and Busted
class SearchBar extends React.Component { constructor(props) { super(props) this.state = { query: '' } } render() { return ( <form action="https://duckduckgo.com"> <input type="text" name="q" onChange={(e) => { this.setState({ query: e.target.value }) }} /> <input type="submit" value="search" /> </form> ) } }
New Hotness
const SearchBar = () => { const [query, setQuery] = useState('') return ( <form action="https://duckduckgo.com" > <input type="text" name="q" onChange={(e) => { setQuery(e.target.value) }} /> <input type="submit" value="search" /> </form> ) }
Thinking in Effects
useRef
is similar. However, working with useEffect
is a bit different. It’s designed to replace several lifecycle methods, such as componentWillReceiveProps
, componentWillMount
, and componentWillUnmoun
t. It doesn’t map one-to-one to old functionality like state and ref, which makes it more difficult to “think in effects.”
Here’s what finally made it click for me: the first two arguments of useEffect
are a function (didUpdate
), and an array of dependencies. the didUpdate function can return a function of its own, a cleanUp
function. When a component mounts or the dependencies are updated, didUpdate
is called. When the component unmounts, cleanUp is called if present.
Once you grok how effects work (“when this variable changes, do something”) over the lifecycle methods, you can save yourself a lot of headaches in code. You no longer have to write code like this:
Old and Busted: componentWillReceiveProps
componentWillReceiveProps = (nextProps) => { if (nextProps.position !== this.props.position) { this.moveMap(nextProps.position) } }
You can now write more succicnt statements like this:
New Hotness: useEffect
useEffect(() => { moveMap(position) }, [position])
Here’s another example is using useEffect
to replace componentDidMount
and componentWillUnmount
for setting and clearing event listeners. By declaring the dependencies array as empty, you only call the didUpdate
and cleanUp
functions once each. No dependencies mean no updates.
Old and Busted: componentWill(un)mount
componentWillMount = () => { /* attach listeners to google StreetView */ const streetView = this.getStreetView() window.google.maps.event.addListener(streetView, 'zoomChanged', this.handlePovChanged()) } componentWillUnmount = () => { window.google.maps.event.clearInstanceListeners() } getStreetView = () => { /* ... */ } handlePovChanged = () => { /* ... */ }
New Hotness: useEffect
const getStreetView = () => { /* ... */ } const handlePovChanged = () => { /* ... */ } const { addListener, clearInstanceListeners } = window.google.maps.event useEffect(() => { const streetView = getStreetView() addListener(streetView, 'on', handlePovChanged()) return clearInstanceListeners }, []) // empty dependency array = only called on mount and unmount
Instead of grouping functionality by when they are in the lifecycle, you can group them by concern. It makes your code easier to reason about.
There’s one problem. You can’t use useEffect
(or any other hook) in a class component. Hooks are only available in functional components. If you want to refactor your lifecycle methods to use useEffect
, you have to refactor entire class components writ large. This is both time-consuming and prone to error. What if you could refactor just this one part of the code?
Use Hooks With Adapter Components
Considering that:
- we cannot use hooks within class components…
- we can use hooks within functional components…
- And we can use functional components within class components…
We can see a solution: create a functional component that encapsulates the useEffect
behavior, and use that in your class components! It’s a take on the adapter pattern from object-oriented programming: We create a wrapper that encapsulates the functionality of a piece of code (useEffect
), while changing its interface. A hook in component’s clothing. ?
Here’s a simple one I use to replace componentWillReceiveProps
checks like the one above.
Observer: useEffect Adapter Component
const observer = ({ value, didUpdate }) => { useEffect(() => { didUpdate(value) }, [value]) return null // component does not render anything }
Example Usage
<Observer value={this.props.position} didUpdate={this.moveMap} />
You don’t have to pass the argument to didUpdate, but in this case I found that the easiest way to have access to that variable. You could also create a similar component to handle mounting and unmounting behavior.
MountHandler: another useEffect Adapter
const mountHandler = ({ onMount, onUnMount }) => { useEffect(() => { onMount() return onUnMount },[]) return null }
You could combine these into one component, or create more specific adapters that encase more business logic themselves. Do what works best for your codebase. Ideally, these a stop-gap solution that help you refactor class-based components incrementally. Here’s the strategy I used:
- Identify a class-based component you want to refactor.
- Isolate part of the class-specific code.
- Create an adapter component that allows you to remove that class specific code (for example, lifecycle methods)
- Run tests, ensure that all functionality still works.
- Repeat until you have removed all class-specific code.
- Convert the component from a class to a function.
In the end, You end up with code thats easier to reason about and more performant. Hope this helps!