Harnessing React Hooks, a practical example

pc_codes

Pascal Ulor

Posted on June 10, 2019

Harnessing React Hooks, a practical example
  1. Introduction
  2. Project Setup
  3. Dependencies
  4. useState
  5. useEffect
  6. Project Links
  7. Resources

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:

  1. Simplicity
  2. 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 usually parent-components
  • Stateless Components: These are class components or functional components that do not declare or manage state. They are usually child-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

Demo
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

Enter fullscreen mode Exit fullscreen mode

Now cd into the instagram-app folder we created and install the following dependencies

cd instagram-app
npm install faker moment styled-components uuid

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

this creates the following folders in our project

Screenshot 2019-06-08 at 11.58.41 PM.png

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
Enter fullscreen mode Exit fullscreen mode

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:

Instagram-clone

Instagram-clone-netlify

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";
Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

Explanation

  1. 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"
  };
});

Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
  1. Inside our App component, we initialized our state. Note the syntax.
  const [posts, setPost] = useState([]);
  const [search, setSearch] = useState("");
Enter fullscreen mode Exit fullscreen mode
  • useState returns a pair of values that represents the current-state (posts) and the update-function that updates the state (setPost and setSearch). setPost and setSearch respectively are similar to the this.setState method of class components.


"The key difference between the
this.setStatemethod 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.

  1. Next, we handle some side effects. Much like the componentDidMount of class components, we will use the useEffect function to mount and render our data from localStorage 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);
  }, []);
Enter fullscreen mode Exit fullscreen mode
  • useEffect takes two arguments. The callback 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>
  );
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Explanation

  • Note that in our PostContainer component the props we received from App.js were rendered as states using the useState 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);
Enter fullscreen mode Exit fullscreen mode
  • We also used the useEffect hook to manage stateful logic and persist our state updates to localStorage.

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])

Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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:

posts

  • The handleChange function calls the setInpuValue function to handle the state of the form input field just like the this.setState method of class components. While the handleLikes function calls the updateLike function to add likes

  • The postComment adds a comment to each post and update the date by calling the setComment and setCreatedAt 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Then our App.js file


  return (
    <AppContainer>
    <ComponentFromWithAuthenticate
        handleSearch={handleSearch}
        search={search}
        posts={posts}
      />
    </AppContainer>
  );

export default App;
Enter fullscreen mode Exit fullscreen mode

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; 

Enter fullscreen mode Exit fullscreen mode

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:

Instagram-clone

Instagram-clone-netlify

Resources

React Documentation
Tom Bowden
James King

💖 💪 🙅 🚩
pc_codes
Pascal Ulor

Posted on June 10, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Functions
react Functions

October 28, 2020