Harnessing React Hooks, a practical example
Pascal Ulor
Posted on June 10, 2019
Introduction
Prerequisites:
This article is for people who are familiar with the basic concepts of react.
Hooks are a powerful feature in the react library that combines react concepts like props, state, context, refs and life-cycle. This feature is supported in React 16.8.0 and above. Hooks were developed for:
- Simplicity
- Performance
Prior to the advent of hooks, one could only declare state in react class components. Besides whenever stateful components
were mentioned in react the only thing that came to mind was a class component
while functional components
were regarded as stateless components
but this is no more the case. Thanks to react hooks functional components
can now declare state and any other react concepts you can think of. Thus react hooks can best be described as follows:
Hooks are functions that let you “hook into” React state and lifecycle features from functional components.
This brings a new distinction to these terms:
-
Stateful Components
: These are class components or functional components that declare and manage state. They are usuallyparent-components
-
Stateless Components
: These are class components or functional components that do not declare or manage state. They are usuallychild-components
Though the react documentations on hooks are well detailed I strongly believe that the best way to grasp a new concept is by doing which is why I have cooked up the mini-project we'll be working on in this article.
Project Setup
To show you how to harness react hooks we'll be building an Instagram clone
together. Below is a live demo of the project
I hope you're as excited as I am
We'll be using create-react-app
for this project. So for starters open you command line and type the following:
npx create-react-app instagram-app
Now cd into the instagram-app folder we created and install the following dependencies
cd instagram-app
npm install faker moment styled-components uuid
Dependencies
-
faker
is an npm package that generates random data inputs -
moment
is an npm package used for date formatting -
styled-components
is an npm package that we'll use to style our components. It utilizes tagged template literals to style your components and eliminates the need for creating CSS files in our project. -
uuid
this is random uuid generator
Now we're going to create our component folders
in your command line type the following
cd src
mkdir -p component/Form component/Login component/PostContainer component/SearchBar component/CommentSection component/Authentication
this creates the following folders in our project
Lets flesh out our components
while in your src folder and type the following
touch component/PostContainer/PostContainer.js component/Form/Form.js component/Login/Login.js component/SearchBar/SearchBar.js component/CommentSection/CommentSection.js
This will create a js file in each component directory respectively.
Since this article is focused on react hooks and its implementation, I'll be going over the code snippets where hooks were used. Which are
- App.js
- PostContainer.js
- Login.js
The link to the complete project repo and hosted app can be found below:
The react hooks we'll be using into in this project are the useState
and useEffect
hooks.
useState
This is called in a functional component
to add some local state to it. This allows us to reuse and share stateful logic in our application.
useEffect
This gives functional components the ability to perform side effects in much the same way as componentDidMount
, componentDidUpdate
and componentWillUnmount
method act in class components.
To use state in our react we must import them as thus:
import React, { useState, useEffect } from "react";
In our App.js file make the following changes
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import uuidv4 from "uuid/v4";
import data from "./dummy-data";
import SearchBar from "./component/SearchBar/SearchBar";
import PostContainer from './component/PostContainer/PostContainer';
const preprocessData = data.map(post => {
return {
...post,
postId: uuidv4(),
show: "on"
};
});
function App() {
const [posts, setPost] = useState([]);
const [search, setSearch] = useState("");
useEffect(() => {
const allData = localStorage.getItem("posts");
let postData;
if (allData) {
postData = JSON.parse(allData);
} else {
localStorage.setItem("posts", JSON.stringify(preprocessData));
postData = JSON.parse(localStorage.getItem("posts"));
}
setPost(postData);
}, []);
const handleSearch = e => {
e.preventDefault();
const data = posts;
setSearch(e.target.value.trim());
const query = data.map(post => {
if (!post.username.trim().toLowerCase().includes(e.target.value.trim())) {
return {
...post,
show: "off"
};
}
return {
...post,
show: "on"
};
});
setPost(query);
};
return (
<AppContainer>
<SearchBar />
<PostContainer />
</AppContainer>
);
}
export default App;
Explanation
- In our App.js file, we imported our raw
data
and tweaked it a bit with the following lines of code
const preprocessData = data.map(post => {
return {
...post,
postId: uuidv4(),
show: "on"
};
});
All this does is give each post in our dummy data a postId and a show
property. We also imported the react hooks we'll be needing
import React, { useState, useEffect } from "react";
- Inside our App component, we initialized our state.
Note
the syntax.
const [posts, setPost] = useState([]);
const [search, setSearch] = useState("");
-
useState
returns a pair of values that represents thecurrent-state
(posts) and theupdate-function
that updates the state (setPost and setSearch).setPost
andsetSearch
respectively are similar to thethis.setState
method ofclass components
.
this.setState
"The key difference between themethod of class components and the update-function of the useState react hook is that it does not merge the old state with the new state"
The useState() method take an argument which is the initial state
(i.e useState([]),useState("")) and is only used in the first render. The argument can be anything from null, a string, a number or an object.
- Next, we handle some side effects. Much like the
componentDidMount
of class components, we will use theuseEffect
function to mount and render our data fromlocalStorage
to state
useEffect(() => {
const allData = localStorage.getItem("posts");
let postData;
if (allData) {
postData = JSON.parse(allData);
} else {
localStorage.setItem("posts", JSON.stringify(preprocessData));
postData = JSON.parse(localStorage.getItem("posts"));
}
setPost(postData);
}, []);
-
useEffect
takes two arguments. Thecallback function
that handles the side effects and an array of the states the effect would have to react to. It is much like adding an event listener to a piece of state. In the above effect, we inputted an empty array as the second argument because we want to call this effect only once when the application starts (just like componentDidMount). If no array is specified the component will rerender on every state change.
Now we need to pass this state to our child components as props.
Make the following update to the JSX of our App.js file
return (
<AppContainer>
<SearchBar search={search} handleSearch={handleSearch} />
{posts.map((userPost, index) => {
return <PostContainer
key={index}
props={userPost}
/>;
})}
</AppContainer>
);
Now PosContainer.js and SearchBar.js need to render the states they have received as props.
In our PostContainer.js file, we'll harness react hooks ability to reuse stateful logic without changing our component hierarchy.
PostContainer.js
const PostContainer = ({ props }) => {
const {
postId,
comments,
thumbnailUrl,
imageUrl,
timestamp,
likes,
username,
show
} = props;
const commentDate = timestamp.replace(/th/, "");
const [inputValue, setInputValue] = useState("");
const [inputComment, setInputComment] = useState(comments);
const [createdAt, setCreatedAt] = useState(
moment(new Date(commentDate), "MMM D LTS").fromNow()
);
const [addLikes, updateLikes] = useState(likes);
useEffect(()=>{
const post = JSON.parse(localStorage.getItem("posts"));
const postUpdate = post.map((userPost) => {
if(postId === userPost.postId) {
return {
...userPost, comments: inputComment, timestamp: `${moment(new Date(), "MMM D LTS")}`, likes: addLikes
}
}
return userPost;
});
localStorage.setItem("posts", JSON.stringify(postUpdate));
},[inputComment, postId, createdAt, addLikes])
const handleChange = e => {
setInputValue(e.target.value);
};
const postComment = e => {
e.preventDefault();
const newComment = {
postId: postId,
id: uuidv4(),
username: faker.name.findName(),
text: inputValue
};
setInputComment([...inputComment, newComment]);
setInputValue("");
setCreatedAt(moment(new Date(), "MMM D LTS").fromNow());
};
const handleLikes = () => {
let newLike = likes;
updateLikes(newLike + 1);
};
return (
<PostContainerStyle display={show}>
<UserDeets>
<UserThumbnail src={thumbnailUrl} alt="user-profile" />
<p>{username}</p>
</UserDeets>
<UserPostArea>
<PostImage src={imageUrl} alt="user-post" />
</UserPostArea>
<Reaction>
<PostIcons>
<span onClick={handleLikes}>
<IoIosHeartEmpty />
</span>
<span>
<FaRegComment />
</span>
</PostIcons>
{addLikes} likes
</Reaction>
{inputComment.map(comment => {
return <CommentSection key={comment.id} props={comment} />;
})}
<TimeStamp>{createdAt}</TimeStamp>
<Form
inputValue={inputValue}
changeHandler={handleChange}
addComment={postComment}
/>
</PostContainerStyle>
);
};
export default PostContainer;
Explanation
-
Note
that in our PostContainer component the props we received from App.js were rendered as states using theuseState
hook.
onst commentDate = timestamp.replace(/th/, "");
const [inputValue, setInputValue] = useState("");
const [inputComment, setInputComment] = useState(comments);
const [createdAt, setCreatedAt] = useState(
moment(new Date(commentDate), "MMM D LTS").fromNow()
);
const [addLikes, updateLikes] = useState(likes);
- We also used the
useEffect
hook to manage stateful logic and persist our state updates tolocalStorage
.
useEffect(()=>{
const post = JSON.parse(localStorage.getItem("posts"));
const postUpdate = post.map((userPost) => {
if(postId === userPost.postId) {
return {
...userPost, comments: inputComment, timestamp: `${moment(new Date(), "MMM D LTS")}`, likes: addLikes
}
}
return userPost;
});
localStorage.setItem("posts", JSON.stringify(postUpdate));
},[inputComment, postId, createdAt, addLikes])
In the useEffect
hook above note the second argument which is an array of states that can trigger the useEffect
function.
[inputComment, postId, createdAt, addLikes]
This means that any change to any of these states will cause the state to be updated in localStorage
.
At this point, our posts should be rendered on the browser like so:
The
handleChange
function calls thesetInpuValue
function to handle the state of the form input field just like thethis.setState
method of class components. While thehandleLikes
function calls theupdateLike
function to add likesThe
postComment
adds a comment to each post and update the date by calling thesetComment
andsetCreatedAt
function respectively.
Wow! Wasn't that fun. Now we can Add comments
and Add Likes
and persist our changes to localStorage
It's time to work on our Login component and create our higher order component for authentication
Login.js
const Login = ({ props }) => {
const [userInput, setUserInput] = useState({
username: "",
password: ""
});
const [loggedIn, setloggedIn] = useState(false);
useEffect(() => {
setloggedIn(true);
}, [userInput.username, userInput.password]);
const loginHandler = () => {
let logDeets = {
username: userInput.username,
password: userInput.password,
loggedIn: loggedIn
};
localStorage.setItem("User", JSON.stringify(logDeets));
};
const handleUserNameChange = e => {
e.persist();
const target = e.target;
const value = target.value;
const name = target.name;
setUserInput(userInput => ({ ...userInput, [name]: value }));
console.log(userInput);
};
return (
<Container>
<Form onSubmit={e => loginHandler(e)}>
<Header>Instagram</Header>
<FormInput
placeholder="Phone number, username or email"
name="username"
type="text"
value={userInput.username}
onChange={handleUserNameChange}
/>
<FormInput
placeholder="Password"
name="password"
type="password"
value={userInput.password}
onChange={handleUserNameChange}
/>
<SubmitBtn type="submit" value="Log In" />
</Form>
</Container>
);
};
export default Login;
Notice how we passed in an object as the useState() argument and how we destructured the state in the setUserInput() function
To add some authentication functionality we will need to create a HOC (higher order component).
Higher Order Components are components that receive components as parameters and returns the component with additional data and functionality. They are pure functions with zero side effects. HOC, as used in this project, is to manage our component render.
We'll start by creating a js file in our authentication
folder and another in our PostContainer
component
touch src/component/PostContainer/PostPage.js src/component/authentication/Authenticate.js
Now we'll do some code refactoring. In our App.js file, we'll cut out the SearchBar component and PostContainer component and paste it into our PostPage.js file.
PostPage.js
import React from 'react';
import SearchBar from "../SearchBar/SearchBar";
import PostContainer from './PostContainer';
const PostPage = ({
handleSearch,
search,
posts
}) => {
return (
<div>
<SearchBar search={search} handleSearch={handleSearch} />
{posts.map((userPost, index) => {
return <PostContainer
key={index}
props={userPost}
/>;
})}
</div>
);
}
export default PostPage;
Then our App.js file
return (
<AppContainer>
<ComponentFromWithAuthenticate
handleSearch={handleSearch}
search={search}
posts={posts}
/>
</AppContainer>
);
export default App;
Then in our Authenticate.js file, we input the following
import React from 'react';
const Authenticate = (WrappedComponent, Login) => class extends React.Component {
render() {
let viewComponent;
if (localStorage.getItem("User")) {
viewComponent = <WrappedComponent {...this.props}/>
} else {
viewComponent = <Login />
}
return (
<div className="container">
{viewComponent}
</div>
)
}
}
export default Authenticate;
And this concludes our mini-project.
Although we only used the useState
and useEffect
hooks (which are the basic and most widely used hooks) you can read about other react hooks and their uses in the react documentation.
Project Links
The link to the complete project repo and hosted app can be found below:
Resources
Posted on June 10, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.