Opinion

Writing reusable components in React using Hooks

Published on 30 Mar, 2021 by Attila

This post is a quick introduction to React Hooks and solves some of the key challenges with it.


React Hooks

`Hooks` is a fairly new addition to the React arsenal that allows you to create purely functional/procedural Single-Page Applications (SPAs) or their Server Side Rendered version.

A quick introduction to Hooks

Below I describe three hooks that I think are the most important to know (useState, useEffect and useCallback), but you can find them all listed with examples on https://reactjs.org/docs/hooks-reference.html.

"useState"

`useState` is used to add state to function based components.

function Comments() { const [comments, setComments] = useState([ { id: 0, message: "Lorem ipsum", likes: 0 } ]); return ( <div> {comments.map((comment) => ( <div key={comment.id}> <div>{comment.message}</div> <div>{comment.likes} likes</div> <button type="button" onClick={() => { comment.likes += 1; setComments((comments) => [...comments]); }} > Like </button> </div> ))} </div> ); }

"useEffect"

`useEffect` is used to make changes in response to changes to the parameters set as the second parameter.

function Comments({ comments, setComments }) { const [viewModel, setViewModel] = useState({ comments }); useEffect( () => setViewModel((viewModel) => ({ ...viewModel, comments: comments.map((comment) => ({ ...comment, likes: `${comment.like} likes` })) })), [comments] ); const like = (commentViewModel) => setComments((comments) => { const commentModel = comments.find( (comment) => comment.id === commentViewModel.id ); commentModel.likes += 1; return [...comments]; }); return ( <div> {viewModel.comments.map((comment) => ( <div key={comment.id}> <div>{comment.message}</div> <div>{comment.likes}</div> <button type="button" onClick={() => like(comment)}> Like </button> </div> ))} </div> ); } ```

"useCallback"

`useCallback` caches the function wrapped in it and the cache is being emptied if the second parameter changes.

function Comments({ comments, setComments }) { const [viewModel, setViewModel] = useState({ comments }); useEffect( () => setViewModel((viewModel) => ({ ...viewModel, comments: comments.map((comment) => ({ ...comment, likes: `${comment.like} likes` })) })), [comments] ); // Cache Forever const like = useCallback( (commentViewModel) => setComments((comments) => { const commentModel = comments.find( (comment) => comment.id === commentViewModel.id ); commentModel.likes += 1; return [...comments]; }), [] ); return ( <div> {viewModel.comments.map((comment) => ( <div key={comment.id}> <div>{comment.message}</div> <div>{comment.likes}</div> <button type="button" onClick={() => like(comment)}> Like </button> </div> ))} </div> ); } ```

What are the challenges with Hooks?

The above `Comments` component is not reusable in any way because it has a `state` attached to it. This raises the following questions?

  • What if I want to render different HTML on mobile?
  • What if I want to use a different `ViewModel` depending on some other logic?
  • What if I want to test the `ViewModel` and state changes without rendering a single `<div>`?
  • What if I want to test the render without messing with the `state`?

The solution: Extracting `state` logic from the component

First, we remove all `state` from the renderable component.

function Comments({ viewModel, like }) { return ( <div> {viewModel.comments.map((comment) => ( <div key={comment.id}> <div>{comment.message}</div> <div>{comment.likes}</div> <button type="button" onClick={() => like(comment)}> Like </button> </div> ))} </div> ); } ```

Secondly, we wrap the `Model` with a `ViewModel`.

function useCommentsViewModel({ comments, addLike }) { const [viewModel, setViewModel] = useState({ comments }); useEffect( () => setViewModel((viewModel) => ({ ...viewModel, comments: comments.map((comment) => ({ ...comment, likes: `${comment.likes} likes` })) })), [comments] ); // Cache Forever const like = useCallback( (commentViewModel) => addLike(commentViewModel.id), [] ); return { viewModel, like }; } ```

Finally, we fetch the model and create a `ViewComponent` and use the functions we created.

function useCommentsModel({ api }) { const [comments, setComments] = useState([]); // Run Once useEffect(() => { async function fetch() { setComments(await api.fetchComments()); } fetch(); }, []); // Cache Forever const addLike = useCallback(async (id) => { await api.addLike(id); setComments((comments) => { const commentModel = comments.find((comment) => comment.id === id); commentModel.likes += 1; return [...comments]; }); }, []); return { comments, addLike }; } function ViewComponent({ api }) { const commentsModel = useCommentsModel({ api }); const commentsViewModel = useCommentsViewModel(commentsModel); return ( <Comments viewModel={commentsViewModel.viewModel} like={commentsViewModel.like} /> ); } ```

Live Demo

View the live demo: https://codesandbox.io/s/react-playground-forked-ic7vv

Takeaway

In conclusion, `React Hooks` allows you to write reusable components and more importantly reusable view related logics while it allows you to test the `state` changes without rending a single `div`.

CHECK OUR OTHER BLOG POSTS

Back to the list