React Hooks Basic
nurfitri •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 are functions that allow a functional component to mimic or make use of lifecycle methods, state, and other React features as if we are using a class-based component.
Here are the list of available React hooks/functions:
- useState: Allows the use of component-level state in functional components.
- useEffect: Allows the use of lifecycle methods in functional components.
- useContext: Allows the use of the context system in functional components.
- useRef: Allows the use of refs.
- useReducer: Allows managing state using 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 use useState() with an initial value of 0. The hook then returns two elements, which we set to variables called count and 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 another line 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 an external API. When we first present 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-based components, we can achieve this behavior using the lifecycle methods. The example below shows how we might use componentDidMount and setState methods in a class-based component to achieve this behavior. We also use the componentDidUpdate method because we implement a nextId button where we fetch the 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 component 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 use the componentDidMount method to initially make a request to the API. Then we use the 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.
Although componentDidUpdate is useful, it can cause problems such as infinite loops when we call fetchUser() because of the setState method in it. Thus, we use the prevProps value to conditionally call fetchUser.
useEffect
We can achieve this same behavior 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 behaviors 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
-
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.
-
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 separate functions. As you may known, hooks is just a function. So lets try refactor our code and create a separate 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 little 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 behavior.
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