Getting baited onto react hooks!
With the latest react version 16.8
react got a whole lot cooler and better with the introduction of hooks! To those that are unaware of what hooks are; they are functions that let you listen or hook onto react state or its life-cycle features by using function components and additionally you could also build your own custom hooks.
The main reason is that hooks allow you to reuse stateful logic without changing your component hierarchy.This allows for decoupling of logic/state from the rendering which helps a lot when testing, reusing and manageability of code.
A more simplified api. With hooks the react life-cycle methods(componentDidMount
, componentDidUpdate
, and componentWillUnmount
) get simplified into one single hook useEffect
.
Offers a better and simplified state management out of the box which significantly reduces deep nesting of components using higher-order components(HOC),render props and context.
No more verbose boilerplate code that comes with class components which more often than not complicates programming flow and code base.With hooks react chooses a direction that embraces the natural functional programming style of javascript which in my opinion is a step in the right direction which gives more freedom for developers to pick and choose how to structure their code , style of programming and design patterns.
Hooks also make react more easier to figure out and adopt to, specially for a newbie as there is no more binding and figuring out this
keyword when it comes to javascript.
Lets take a typical react class component and try to convert it to a functional component. Assuming we are given a scenario to fetch GitHub information of a given user name and display the details a react class component would typically look like this...
import React from "react";
class GitHubProfile extends React.Component {
constructor(props) {
super(props);
this.state = { gitHubUserInfo: null, inputUserName: "dasithKuruppu" };
}
componentDidMount() {
this.queryGetGitHubInfo();
}
queryGetGitHubInfo = async (userName = "dasithKuruppu") => {
let response = await fetch(`https://api.github.com/users/${userName}`);
let jsonResponse = await response.json();
this.setState({ ...this.state, gitHubUserInfo: jsonResponse });
};
displayNameButtonClick = () => {
this.queryGetGitHubInfo(this.state.inputUserName);
};
onInputTextChange = e => {
this.setState({ ...this.state, inputUserName: e.currentTarget.value });
};
render() {
return (
<div>
<h1>
Hello, {this.state.gitHubUserInfo && this.state.gitHubUserInfo.name}
</h1>
<input
value={this.state.inputUserName}
onChange={this.onInputTextChange}
/>
<button onClick={this.displayNameButtonClick}>
Fetch Github information
</button>
<ul>
{this.state.gitHubUserInfo &&
Object.keys(this.state.gitHubUserInfo).map(key => {
return (
<li key={key}>
{`${key} : ${this.state.gitHubUserInfo[key]}`}
</li>
);
})}
</ul>
</div>
);
}
}
An equivalent functional component to the class component above would look like the following...
import React, { useState, useEffect } from "react";
function GitHubProfile() {
const [inputUserName, setInputUserName] = useState("dasithKuruppu");
const [gitHubUserInfo, getGitHubUserInfo] = useGetGitHubInfo(null);
const displayNameButtonClick = () => {
getGitHubUserInfo(inputUserName);
};
const onInputTextChange = e => {
setInputUserName(e.currentTarget.value);
};
return (
<div>
<h1>Hello, {gitHubUserInfo && gitHubUserInfo.name}</h1>
<input value={inputUserName} onChange={onInputTextChange} />
<button onClick={displayNameButtonClick}>Fetch Github Information</button>
<ul>
{gitHubUserInfo &&
Object.keys(gitHubUserInfo).map(key => {
return <li key={key}>{`${key} : ${gitHubUserInfo[key]}`} </li>;
})}
</ul>
</div>
);
}
Notice how cleaner and better it looks with all the boilerplate code removed.Here it is really simplified with state hooks(useState
), by using state hooks you can directly set state to a localized version of state for each hook.
const [inputUserName, setInputUserName] = useState("dasithKuruppu");
The above line initializes state with the default string "dasithkuruppu"
by calling useState()
which returns an array with 2 elements which can destructured to [inputUserName,setInputUserName]
only difference being that here the setInputUserName
is more or less localized only to the state initialized using useState()
. When it comes to the above inputUserName
is the state while setInputUserName
is the function that allows us to set the state.
const [gitHubUserInfo, getGitHubUserInfo] = useGetGitHubInfo(null);
This line does something similar except there is the missing piece, this uses a custom hook which allows us to decouple and abstract state away from components and reuse them easily. The custom hook for this can be written like this.
function useGetGitHubInfo() {
const [gitHubUserInfo, setGitHubUserInfo] = useState(null);
useEffect(() => {
getGitHubUserInfo();
}, []);
const getGitHubUserInfo = async (userName = "dasithkuruppu") => {
let response = await fetch(`https://api.github.com/users/${userName}`);
let jsonResponse = await response.json();
setGitHubUserInfo(jsonResponse);
};
return [gitHubUserInfo, getGitHubUserInfo];
}
This hook could be used on any component that needs GitHub user information with barely any boilerplate code or a higher order component(HOC).You may now be thinking what kind of sorcery this is... well its partly right as the new hooks api requires a mind shift from the typical react flow and at first it may seem like there is a lot of magic happening under the hood.However explaining what happens here might clear things up a bit , first things first a custom hook is also capable of having hooks to state and react lifecycle features !
const [gitHubUserInfo, setGitHubUserInfo] = useState(null);
This piece of code as you guessed initializes a state for gitHubUserInfo
.
useEffect(() => {
getGitHubUserInfo();
}, []);
This is the equivalent of componentDidMount and if you happen to return something on the function passed to useEffect
it would be the equivalent of componentWillUnmount. And on the 2nd argument to useEffect it takes an array of items that React will keep track of and if any of it changes the function passed to useEffect
would be executed, you can do this if you want it to behave somewhat similar to componentDidUpdate. However in this case we have passed an empty array as the second argument to useEffect
(equivalent of componentDidMount) and within the function passed to it we have executed getGitHubUserInfo()
, which fetches GitHub user information from a public endpoint and calls setGitHubUserInfo
with the response which as I explained before sets the state gitHubUserInfo
. Finally we return an array with the state gitHubUserInfo
and the async function getGitHubUserInfo
which can be called on each button click.
The rest of the code on the GitHubProfile
is pretty self explanatory, its basically plugging in state and the button click event handler onto the JSX. Similar to what was on the class component except that this
reference is gone and only variables/const are used. In case you have a lot of state / actions on a component It could be also wise to separate out this logic from the component. You can do this easily with useReducer
if you were to replace useState
with useReducer
in the previous example it would look something similar to this
import React, { useEffect, useReducer } from "react";
async function getGitHubInfo(dispatch, actionName = "setGitHubUserInfo") {
const getGitHubUserInfo = async (userName = "dasithkuruppu") => {
let response = await fetch(`https://api.github.com/users/${userName}`);
return await response.json();
};
const payload = await getGitHubUserInfo();
dispatch({ type: actionName, payload });
}
const reducer = (state, action) => {
switch (action.type) {
case "setGitHubUserInfo":
return { ...state, gitHubUserInfo: action.payload };
case "setInputUserName":
return { ...state, inputUserName: action.payload };
default:
return state;
}
};
const initialState = {
GitHubUserInfo: null,
InputUserName: "dasithkuruppu"
};
function GitHubProfile(props) {
const [{ inputUserName, gitHubUserInfo }, dispatch] = useReducer(
reducer,
initialState
);
useEffect(() => {
getGitHubInfo(dispatch);
}, []);
const displayNameButtonClick = () => {
getGitHubInfo(dispatch);
};
const onInputTextChange = e => {
dispatch({ type: "setInputUserName", payload: e.currentTarget.value });
};
return (
<div>
<h1>Hello, {gitHubUserInfo && gitHubUserInfo.name}</h1>
<input value={inputUserName} onChange={onInputTextChange} />
<button onClick={displayNameButtonClick}>Fetch Github information</button>
<ul>
{gitHubUserInfo &&
Object.keys(gitHubUserInfo).map(key => {
return <li key={key}>{`${key} : ${gitHubUserInfo[key]}`} </li>;
})}
</ul>
</div>
);
}
This looks like a lot of code compared to useState
but that gives us the advantage of separating state from component entirely like using redux without redux ! A reducer helps us to understand code better as opposed to having many useState calls in the component itself which can make the component look a lot more complex than it actually has to. This is also a lot similar to redux and you can actually implement a redux style state management by using useReducer
and the useContext
(context api) without actually using redux or its additional boilerplate / higher order components.However this is a topic for another article.In this article I only hoped to cover the basics of hooks and hope you learned something. Let me know if you have comments / feedback below...
You can find working versions of of the code above on a sandbox Hooks with useReduce and Hooks with useState.