React-Hooks! What's special in using them?
Let's quickly explain how hooks work in React.
Hooks allow us to "hook" into React features such as state and lifecycle methods.
Hooks allow function components to have access to state and other React features. Because of this, class components are generally no longer needed.
Although Hooks generally replace class components, there are no plans to remove classes from React. Hooks were added to React in version 16.8.
There are 10 in-built hooks, and this is how you can perfectly use them in your project.
useState:
The React
useState
Hook allows us to track state in a function component. State generally refers to data or properites that need to be tracking in an application.
To use the useState
Hook, we first need to import it into our component at the top of the component. import { useState } from "react";
State can be initialized by calling useState
in our function component.
useState
accepts an initial state and returns two values which are the current state and a function that updates the state.
Always Initialize state at the top of the function component.
Code Sample of how to initialize state:
import { useState } from "react";
function FavoriteColor() {
const [color, setColor] = useState("");
}
Note: We are destructuring the returned values from useState
. The first value, color
, is our current state. While, the second value, setColor
, is the function that is used to update our state. We now set the initial state to an empty string useState("")
You can name the variables anything you would like.
Let's include our states anywhere by reading the state and using the state variable in the rendered component.
Code Sample:
import { useState } from "react";
import ReactDOM from "react-dom";
function FavoriteColor() {
const [color, setColor] = useState("red");
return <h1>My favorite color is {color}!</h1>
}
ReactDOM.render(<FavoriteColor />, document.getElementById('root'));
You can also update the state by using state updater function.
Note: Don't directly update a state.
Code Sample of how to update state:
import { useState } from "react";
import ReactDOM from "react-dom";
function FavoriteColor() {
const [color, setColor] = useState("red");
return (
<>
<h1>My favorite color is {color}!</h1>
<button
type="button"
onClick={() => setColor("blue")}
>Blue</button>
</>
)
}
ReactDOM.render(<FavoriteColor />, document.getElementById('root'));
useState
Hook can be used to keep track of strings, numbers, booleans, arrays, objects, and any combination of these!
Code Sample:
import { useState } from "react";
import ReactDOM from "react-dom";
function Car() {
const [car, setCar] = useState({
brand: "Ford",
model: "Mustang",
year: "1964",
color: "red"
});
return (
<>
<h1>My {car.brand}</h1>
<p>
It is a {car.color} {car.model} from {car.year}.
</p>
</>
)
}
ReactDOM.render(<Car />, document.getElementById('root'));
You may be thinking that how can you update objects and arrays in state? No worries. We can use the JavaScript spread operator to help us get it done with ease.
Code Sample:
import { useState } from "react";
import ReactDOM from "react-dom";
function Car() {
const [car, setCar] = useState({
brand: "Ford",
model: "Mustang",
year: "1964",
color: "red"
});
const updateColor = () => {
setCar(previousState => {
return { ...previousState, color: "blue" }
});
}
return (
<>
<h1>My {car.brand}</h1>
<p>
It is a {car.color} {car.model} from {car.year}.
</p>
<button
type="button"
onClick={updateColor}
>Blue</button>
</>
)
}
ReactDOM.render(<Car />, document.getElementById('root'));
Because we need the current value of state, we pass a function into our setCar
function. This function receives the previous value. We then return an object, spreading the previousState
and overwriting only the color
. I believe this will help and you can also drop a comment if you want to know more about JavaScript spread operator.
useEffect:
The useEffect
Hook allows you to perform side effects in your components. Some examples of side effects are: fetching data, directly updating the DOM, and timers.
useEffect
accepts two arguments. The second argument is optional. Example: useEffect
(<function>
, <dependency>
)
Code Sample:
import { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Counter() {
const [count, setCount] = useState(0);
const [calculation, setCalculation] = useState(0);
useEffect(() => {
setCalculation(() => count * 2);
}, [count]); // <- add the count variable here
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<p>Calculation: {calculation}</p>
</>
);
}
ReactDOM.render(<Counter />, document.getElementById("root"));
useMemo:
The React useMemo
Hook returns a memoized value. Memoization is just like caching a value so that it does not need to be recalculated.
The useMemo
Hook only runs when one of its dependencies update.
The useMemo
and useCallback
Hooks are similar. The main difference is that useMemo
returns a memoized value and, useCallback
returns a memoized function.
Note: The useMemo
Hook can be used to keep expensive, resource intensive functions from needlessly running.
Code Sample:
import { useState, useMemo } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = useMemo(() => expensiveCalculation(count), [count]);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => [...t, "New Todo"]);
};
return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>Add Todo</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</div>
);
};
const expensiveCalculation = (num) => {
console.log("Calculating...");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
ReactDOM.render(<App />, document.getElementById("root"));
useContext:
React Context is a way to manage state globally. It can be used together with the
useState
Hook to share state between deeply nested components more easily than withuseState
alone.
Note: State should be held by the highest parent component in the stack that requires access to the state
.
Code Sample:
import { useState, createContext, useContext } from "react";
import ReactDOM from "react-dom";
const UserContext = createContext();
function Component1() {
const [user, setUser] = useState("Jesse Hall");
return (
<UserContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 user={user} />
</UserContext.Provider>
);
}
function Component2() {
return (
<>
<h1>Component 2</h1>
<Component3 />
</>
);
}
function Component3() {
return (
<>
<h1>Component 3</h1>
<Component4 />
</>
);
}
function Component4() {
return (
<>
<h1>Component 4</h1>
<Component5 />
</>
);
}
function Component5() {
const user = useContext(UserContext);
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}
ReactDOM.render(<Component1 />, document.getElementById("root"));
useCallback:
The React useCallback
Hook returns a memoized callback function. Memoization is just like caching a value so that it does not need to be recalculated.
This allows us to isolate resource intensive functions so that they will not automatically run on every render. The useCallback
Hook only runs when one of its dependencies update.
The useCallback
and useMemo
Hooks are similar.
The main difference is that useMemo
returns a memoized value and useCallback
returns a memoized function.
Note: One reason to use useCallback
is to prevent a component from re-rendering unless its props have changed.
Code Sample:
import { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Todos from "./Todos";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = useCallback(() => {
setTodos((t) => [...t, "New Todo"]);
}, [todos]);
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
useReducer:
If you find yourself keeping track of multiple pieces of state that rely on complex logic, useReducer
may be useful. It allows for custom state logic.
The useReducer
Hook is similar to the useState
Hook.
The useReducer
Hook accepts two arguments.
useReducer
(<reducer>
, <initialState>
)
The reducer
function contains your custom state
logic and the initialState
can be a simple value but generally will contain an object.
The useReducer
Hook returns the current state and a dispatch
method.
Sample Code:
import { useReducer } from "react";
import ReactDOM from "react-dom";
const initialTodos = [
{
id: 1,
title: "Todo 1",
complete: false,
},
{
id: 2,
title: "Todo 2",
complete: false,
},
];
const reducer = (state, action) => {
switch (action.type) {
case "COMPLETE":
return state.map((todo) => {
if (todo.id === action.id) {
return { ...todo, complete: !todo.complete };
} else {
return todo;
}
});
default:
return state;
}
};
function Todos() {
const [todos, dispatch] = useReducer(reducer, initialTodos);
const handleComplete = (todo) => {
dispatch({ type: "COMPLETE", id: todo.id });
};
return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.complete}
onChange={() => handleComplete(todo)}
/>
{todo.title}
</label>
</div>
))}
</>
);
}
ReactDOM.render(<Todos />, document.getElementById("root"));
useRef:
The useRef
Hook allows you to persist values between renders. It can be used to store a mutable value that does not cause a re-render when updated. It can be used to access a DOM element directly.
If we tried to count how many times our application renders using the useState
Hook, we would be caught in an infinite loop since this Hook itself causes a re-render. To avoid this, we can use the useRef
Hook.
useRef
hook does not cause re-render.
Code Sample:
import { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
function App() {
const [inputValue, setInputValue] = useState("");
const previousInputValue = useRef("");
useEffect(() => {
previousInputValue.current = inputValue;
}, [inputValue]);
return (
<>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h2>Current Value: {inputValue}</h2>
<h2>Previous Value: {previousInputValue.current}</h2>
</>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
useDebugValue:
It allows us to define our own custom labels in React Dev Tools, which are useful for debugging.
Code Sample:
function useDisplayName() {
const [displayName, setDisplayName] = React.useState();
React.useEffect(() => {
const data = fetchFromDatabase(props.userId);
setDisplayName(data.displayName);
}, []);
React.useDebugValue(displayName ?? 'loading...');
return displayName;
}
useImperativeHandle:
This hook is use to modify the exposed ref and it is rarely used.
Code Sample:
function useImperativeHandleDemo(props, ref) {
const myBtn = useRef(null);
React.useImperativeHandle(ref, () => ({
click: () => {
console.log('clicking button!');
myBtn.current.click();
}
}));
}
useLayoutEffect:
It works the same as useEffect
hook with one difference, the callback will run after rendering the component but before the actual updates have been painted to the screen. Blocks visual updates until your callback is finished.
Code Sample:
function useLayoutEffectDemo() {
const myBtn = React.useRef(null);
React.useLayoutEffect(() => {
const rect = myBtn.current.getBoundingClientRect();
// scroll position before the dom is visually updated
console.log(rect.height);
})
}
###How To Use React Custom Hooks
When you have component logic that needs to be used by multiple components, we can extract that logic to a custom Hook.
Custom Hooks start with "use". Example: useFetch
.
Hooks are reusable functions.
Let's build a hook. In the following code, we are fetching data in our Home component and displaying it.
We will use the JSONPlaceholder service to fetch fake data. This service is great for testing applications when there is no existing data.
Code Sample:
Place this code below in your index.js.
import { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Home = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return (
<>
{data &&
data.map((item) => {
return <p key={item.id}>{item.title}</p>;
})}
</>
);
};
ReactDOM.render(<Home />, document.getElementById("root"));
The fetch logic may be needed in other components as well, so we will extract that into a custom Hook.
Now, move the fetch logic to a new file to be used as a custom Hook:
Create a new file and name it useFetch or anything you like, then place this code below:
Code Sample:
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data));
}, [url]);
return [data];
};
export default useFetch;
In your index.js
file now import the useFetch
file. Check the sample below for better understanding:
import ReactDOM from "react-dom";
import useFetch from "./useFetch";
const Home = () => {
const [data] = useFetch("https://jsonplaceholder.typicode.com/todos");
return (
<>
{data &&
data.map((item) => {
return <p key={item.id}>{item.title}</p>;
})}
</>
);
};
ReactDOM.render(<Home />, document.getElementById("root"));
Let's make a better explanation to the code above:
Firstly, we have created a new file called useFetch.js
containing a function called useFetch
which contains all of the logic needed to fetch our data.
We now removed the hard-coded URL and replaced it with a url variable that can be passed to the custom Hook.
Lastly, we are returning our data from our Hook.
In index.js
, we are importing our useFetch
Hook and utilizing it like any other Hook. This is where we pass in the URL to fetch data from.
Now we can reuse this custom Hook in any component to fetch data from any URL.