It's been quite some time since React hooks were introduced. It is a brand new primitive to compose things, and this new primitive tries to put an end to many anti-patterns (for good).
From the composition point of view, React hooks is the biggest change in React till now and given the patterns which had emerged since last few years, this change was long due. In this article, I try to cover some problems I used to face in my code and when replaced them with a hook it became so much more better.
Managing side effects using lifecycle methods
A common pattern is to perform side effects in one of the lifecycle methods. This can lead to a mix of imperative and declarative code which slowly becomes very hard to follow and debug. With useEffect hook (and others as well) it becomes a lot easier to manage to this separation of concerns.
class Example extends Component {
state = {
data: null
};
componentDidMount() {
axios.get('some/remote/data').then((res) => {
this.setState({data: res})
});
}
render() {
// render DOM using data
}
}
Using hooks we can separate out side-effect
function useRemoteData() {
const [data, setData] = useState(null);
useEffect(() => {
axios.get('some/remote/data').then((res) => {
setData(res)
});
}, []);
return data;
}
And our Example
component is left to just this simple function! Turns out, class components were really hard to read. Well, who needs them when we can do all this with plain functions.
function Example() {
const data = useRemoteData();
//render DOM using data
}
Let's add some more side effects to our recipes. In the original example using class we now need to listen to a PubSub event, and use the event data to query API.
class Example extends Component {
state = {
data: null,
query: ''
};
componentDidMount() {
this.loadData();
PubSub.addListener('event', this.handleEvent);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.query !== this.state.query) {
this.loadData();
}
}
componentWillUnmount() {
PubSub.removeListener(this.handleEvent);
}
loadData = () => {
axios.get(`some/remote/data?${this.state.query}`).then((res) => {
this.setState({data: res})
});
}
handleEvent = query => {
this.setState({query})
}
render() {
// render DOM using data
}
}
Our hooks now change to
function useRemoteData(q) {
const [data, setData] = useState(null);
useEffect(() => {
axios.get('some/remote/data').then((res) => {
setData(res)
});
}, [q]);
return [data]
}
function usePubSub() {
const [query, setQuery] = useState('');
useEffect(() => {
function handleEvent(q) {
setQuery(q);
}
PubSub.addListener('event', handleEvent);
return () => {
PubSub.removeListener(handleEvent);
}
}, []);
return query;
}
And our Example
component is still a plain function.
function Example() {
const query = usePubSub();
const data = useRemoteData(query);
//render DOM using data
}
So clean and separate. Many components can now benefit from these hooks. As you can easily see with just 2 side effects it's starting to become complex using our class based components.
But what about higher order components or render-props
One might argue that using higher order components or render-props pattern will also untangle these complexities. But then they bring their own set of problems.
- Access to component state - A parent HOC doesn't have access to wrapped component's state.
- Naming clashes - Since HOCs inject props into wrapped component, chances of naming clashes between props arises. Even if props are name-spaced, if the same HOC is used more than once, naming clashes have to be handled separately.
- Code-parsers - Since there are many ways HOCs can be composed, its difficult to statically analyse code.
- Layers of HOCs - When there are lot of HOCs above a component, tracking down which prop is coming from which HOC is difficult. Also during debugging it becomes a nightmare to find out which component is causing re-rendering.
- False hierarchy - These patterns add false hierarchy to component tree and creates component hell.
- Anonymous Arrow Functions - Render props makes heavy use of arrow functions, and specially if your component tree is huge, it can lead to lots re-renders which can eventually hamper the performance.
- Forwarding refs - In an ideal world of templating, there would be one-to-one mapping between React component tree and DOM. So that there is no need to forward refs.
Conclusion
So hooks look like very useful primitives. It changes the way we reason about components and composing the various other primitives. Many popular UI component libraries have already adopted it in their latest version. It will be interesting to see how other libraries and frameworks catch up.
For now though, I'm pretty much hooked😆
Peace and enjoy this song.
Top comments (0)