

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`.

