React Hooks Basic

nurfitri
React Hooks Basic

React Hooks

When we use React in a class base component, we have access to the state and lifecycle method. This is not the case for functional component in react. To get the same capabilities as class base component, React introduced hooks .

Hooks

Hooks is basically a functions that allows a functional component to mimic or make use of the lifecycle method and state and other as if we are using a class base component.

Here are the list of available react hooks/functions:

  • useState, allow to use component-level state in functional component.
  • useEffect, allow to use lifecycle methods in functional component.
  • useContext, allow to use context system in functional component.
  • useRef, allow to use ref .
  • useReducer, allow to store data tru a reducer.

State With Hooks

Lets take a look at state example in a class component. Here in our class component we make use of the state and setState from React.Component to manage the state of our component.

class Main extends React.Component{
	state = {
		count: 0,
	}
	increaseCounter(){
		this.setState({
			count: this.state.count + 1
		})
	}
	render(){
		return (
			<div>
				<h1>clicks count: {this.state.count} </h1>
				<button onClick={this.increaseCounter.bind(this)}>+</button>
			</div>
		)
	}
}

ReactDOM.render(<Main/>, document.querySelector('#root'));

In a functional component, because we are not extending the React.Component, we have no access to state property and useState method to manage our component. To overcome this problem, we use useState hook.

This hook take a initial value as an argument, and return an array with two elements. The array contains a reference to the current value of our state and a reference to a setter function to set the value for our state.

useState

Here are the functional component example

const {useState} = React; // import {useState} from 'React';
function Main (){
	// we initialize our state with number 0.
	// then the hook returns a property that we assigned named as `count`
	// and a set mehod that we named as `setCount`.
	const [count,setCount] = useState(0); //array destructuring 
	function increaseCounter(){
		setCount(count+1);
	}
	return (
		<div>
			<h1>clicks count: {count} </h1>
			<button onClick={increaseCounter}>+</button>
			<button onClick={()=> setCount(0)}>Reset</button>
		</div>
	) 
}
ReactDOM.render(<Main/>, document.querySelector('#root'));

full code and demo on codesandbox

In the example above, we make use of the useState() giving the initial value of 0. The hook then return two elements which we set the first element to a variable called count and the second element to a variable called setCount. Instead of using setState to set the value of a state, we now use setCount or any name we give to it.

If we want to add another state, we simply add otherline for that state like this

function Main(){
	const [count,setCount] = useState(0);
	const [isMax,setIsMax] = useState(false); //here we add another state
	...

}

the functional example above is equivalent to the class example below

class Main extends React.Component{
	state = {
		count: 0,
		isMax: false,
	}
}

refresher on state

NOTE: Whenever we called setState or in this functional component example setCount to set our state. The whole component get re-render and get updated with the new value.

Lifecycle with Hooks

Let's say we are fetching something from external API. When we first presented with the page, we might want to fetch data from an API. It will take a little time for the data to get fetched but when we get the data, we need to change the state of our component.

In class base component we can achieve this behaviour using the lifecycle methods . Example below are how we might uses componentDidMount and setState methods in a class base component to achieve the behaviour. We also make use of componentDidUpdate method because we implement a nextId button where we fetch next data when we click on the button (think of this like a pagination example ).

class Main extends React.Component{
	state = {
		id: 1,
	}
	changeId(n){
		this.setState({
			id: this.state.id + n,
		})
	}

	render(){
		return (
			<div>
				<h1>User: {this.state.id} </h1>
				<button onClick={()=> this.changeId(-1)}> PrevUser </button>
				<button onClick={()=> this.changeId(1)} > NextUser </button>
				<UserDetails id={this.state.id}/>
			</div>
		)
	}
}

class UserDetails extends React.Component{
	state = {
		user: null,
	}
	
	async fetchUser(){
		const response = await 
		(await fetch(
		`https://jsonplaceholder.typicode.com/users/${this.props.id}`
		)).json();
		this.setState({
			user:response
		});
	}
	// this get called once when component mount.
	componentDidMount(){
		this.fetchUser();
	}
	
	/**
	we need to call fetchUser again when the id of parent component get updated.
	this will also get call when we our cmponent get re-render 
	when we run setState.
	**/
	componentDidUpdate(prevProps){
		/**
		componentDidUpdate have a param prevProps which we can use to reference
		previous props. We can use it to check if our id props change
		then we call fetchUser. This is to prevent infinite oops. 
		due to re-render when we use setState.
		**/
		if(prevProps.id !== this.props.id){
			this.fetchUser();
			console.log('Getting User..!');
		}
	}

	render(){
		return (
			<div>
				{
					this.state.user &&
					<ul>
						<li>ID: {this.state.user.id}</li>
						<li>Name: {this.state.user.name}</li>
						<li>UserName: {this.state.user.username}</li>
						<li>Email: {this.state.user.email}</li>
					</ul>
				}
			</div>
		)
	}
}

ReactDOM.render(<Main/>, document.querySelector('#root'));

As we can see, we first make use of the componentDidMount method to first making a request to the API. Then we use setState method to set the state and re-render our page with the newly fetched data. Without componentDidUpdate, if we click the NextId button, the UserDetails component will not fetch a user because we only call fetchUser once in componentDidMount.

Tho, componentDidUpdate is usefull, it can caused a problem such as infinite loop when we call fetchUser() because of the setState method in it. Thus, we make use of prevProps value to conditionally call fetchUser.

useEffect

We can achieve this same behaviour in a functional component using the useEffect and useState hooks.

const {useState, useEffect} = React; // import {useState,useEffect} from 'React';

function Main(){
	const [id,setId] = useState(1);
	const changeId = (n) => {
		setId(id+n);
	}
	return <div>
		<h1>User: {id} </h1>
		<button onClick={()=> changeId(-1)}> PrevUser </button>
		<button onClick={()=> changeId(1)} > NextUser </button>
		<UserDetails id={id}/>
	</div>
}

function UserDetails(props){
	const [user,setUser] = useState(null);
	
	const fetchUser = async () => {
		const response = await 
		(await fetch(
		`https://jsonplaceholder.typicode.com/users/${props.id}`
		)).json();
		setUser(response);
	}

	//here as simple as that
	useEffect(()=>{
		fetchUser();
	},[props.id]);
	
	return <div>
		{
			user && 
			<ul>
				<li>ID: {user.id}</li>
				<li>Name: {user.name}</li>
				<li>UserName: {user.username}</li>
				<li>Email: {user.email}</li>
			</ul>
		}
	</div>
}

ReactDOM.render(<Main/>, document.querySelector('#root'));

full code and demo on codesandbox

Lets understand how useEffect() works. When we set our useEffect like this

...
useEffect(()=>{
	fetchUser();
},[props.id]) // watching the value 
...

The useEffect hooks take two arguments. A callback function and an array contains values to watch for.

During the first render it first call the callback containing fetchUser() function. then if the component get re-render it watch for any changes of values inside the array, and anytime the value change, it will re-run the callback.

This behavious is just the same as we previously did using class base component when we check for prevProps values. Here we check for the id props value wether it is the same as the previous value. if the value changed then the callback is called.

Using useEffect is actually pretty clean if you ask me. We are able to combine both lifecycle method componentDidMount and componentDidUpdate into single hooks function.

Note on useEffect

  1. What array ? if we pass in an empty array to the hooks, the callback function will only run once.

    useEffect(()=>{
    	console.log('running once!');
    	fetchUser();
    },[])
    

    if we pass in an object to the array, then the callback will get call in an infinite loop, event tho we are not changing the value of the object.

    useEffect(()=>{
    	console.log('running multiple times!');
    	fetchUser();
    },[{id:props.id}]);
    

    This effect is due to redux reducer, where everytime we create an object in javascript, different object were created in memory. Thus, everytime we re-render the page. A new object being pass to the array even if we not changing it's value.

    In short, anytime the value in the array get change, the callback will get called.

  2. Async callback ? useEffect doesn't allowed for declaring async function as the callback function.

    useEffect(
    	// this is not OK!
    	async () => {
    		const response = await 
    		(await fetch(
    		`https://jsonplaceholder.typicode.com/users/${props.id}`
    		)).json();
    		setUser(response);
    	
    },[props.id])
    

    If we run the above code, we will get an error

    Warning: useEffect must not return anything besides a function, which is used 
    for clean-up.
    

    The callback functions must not return anything beside a function, if we declare as a async, it will return a promise.

    This is also not an OK behaviour

    const fetchUser = async () => {
    	const response = await 
    	(await fetch(
    	`https://jsonplaceholder.typicode.com/users/${props.id}`
    	)).json();
    	setUser(response);
    }
    //this is not ok,
    useEffect(fetchUser,[props.id]);
    

    Instead we need to execute the async function inside a regular function like we previously did or like this.

    useEffect(()=>{
    	const fetchUser = async () => {
    		const response = await 
    		(await fetch(
    		`https://jsonplaceholder.typicode.com/users/${props.id}`
    		)).json();
    		setUser(response);
    	}
    	fetchUser();
    },[props.id]);
    

Create Our Own Custom Hooks

Using above example, we can make our code more cleaner and reusable by extracting some functionalities into seperate functions. As you may known, hooks is just a function. So lets try refactor our code and create a seperate hooks for fetching user.

const {useState, useEffect} = React; // import {useState,useEffect} from 'React';

// here we define our custom hooks to fetch user.
// we detached the code from the UserDetails Component, so that 
// it can be re-usable by another component.
// our UserDetails component end up becoming more clean.

function useUser(id){
	const [user,setUser] = useState(null);
	//here as simple as that
	useEffect(()=>{
		(async () => {
			const response = await 
			(await fetch(
			`https://jsonplaceholder.typicode.com/users/${id}`
			)).json();
			setUser(response);
		})();
		
	},[id]);

	//here we return fetched user
	return user;
}

function Main(){
	const [id,setId] = useState(1);
	const changeId = (n) => {
		setId(id+n);
	}
	return <div>
		<h1>User: {id} </h1>
		<button onClick={()=> changeId(-1)}> PrevUser </button>
		<button onClick={()=> changeId(1)} > NextUser </button>
		<UserDetails id={id}/>
	</div>
}

// we detached all those useState, and useEffect hooks 
// making it into a single hooks, thus making our component
// more clean.
function UserDetails(props){
	// here we call our custom hooks 
	const user = useUser(props.id);	
	return <div>
		{
			user && 
			<ul>
				<li>ID: {user.id}</li>
				<li>Name: {user.name}</li>
				<li>UserName: {user.username}</li>
				<li>Email: {user.email}</li>
			</ul>
		}
	</div>
}

ReactDOM.render(<Main/>, document.querySelector('#root'));

full code and demo on codesandbox

Ref with hooks

in react, ref system allows us the developer, to access DOM element just like we access it in vanilla js.

const el = document.querySelector("#myElement");

To access the dom in a class component is to use the React.createRef() method and assign it to a variable. Then we reference that variable from the jsx element.

class Main extends React.Component{
	constructor(props){
		super(props);
		this.myRef = React.createRef();
	}

	render(){
		return (
			<div>
				<input ref={this.myRef}>
			</div>
		)
	}
}

Let's use our UserDetails example, from create custom hooks example, and change it a litle bit to make use of ref. For the sake of simplicity, we are only changing the Main component.

In a class base component it will look like this

class Main extends React.Component{
		constructor(props){
		super(props);
		this.state = {
			id: 1,
		}
		// here we declare our reference
		this.idsRef = React.createRef();
		this.ids = [...Array(10).keys()];
	}

	changeId(){
		// here we accessing the DOM value.
		const value = this.idsRef.current.value;
		if(value !== ''){
			this.setState({
				id: value,
			})
		}
	}

	render(){
		return (
			<div>
				<h1>User: {this.state.id}</h1>
				<label htmlFor="ids" >Select id: </label>
				<select
					onChange={this.changeId.bind(this)}
					ref={this.idsRef} //we use ref
					name="ids"
					id="ids"
				>
					{
						this.ids.map(id=>(
							<option key={id+1} value={id+1}>
								{id+1}
							</option>
						))
					}
				</select>
				<UserDetails id={this.state.id} />
			</div>
		)
	}
}

useRef

For a functional component example, we use useRef hooks to implement the same behaviour.

const {useState,useRef} = React;
function Main(){
	const [id,setId] = useState(1);

	//here we declare our reference
	const idsRef = useRef('');
	const ids = [...Array(10).keys()];

	const changeId = () =>{
		const value = idsRef.current.value;
		if(value !== ''){
			setId(value);
		}
	};

	return (
		<div>
			<h1>User: {id}</h1>
			<label htmlFor="ids" >Select id: </label>
			<select
				onChange={changeId}
				ref={idsRef} // here we use ref
				name="ids"
				id="ids"
			>
				{
					ids.map(id=>(
						<option key={id+1} value={id+1}>
							{id+1}
						</option>
					))
				}
			</select>
			<UserDetails id={id} />
		</div>
	)
}

As simple as that. full code and demo on codesandbox

End ?

In my next post, I will talk some more about context and redux, and I will be showing some example on how to use context in class and functional component.