Hooks are a relatively new way (React v16.8.x and up) to add state and lifecycle to functional components. Before hooks, you needed to use a class to have these same features. However, using classes in Javascript has its own set of issues:
- Some new devs might not have an OO background
- What's
this
for again? - private vs public vs static???
- More complicated to share functionality
- Transpilers will convert classes to regular functions anyways
I've noticed that many developers prefer writing components as functional components as opposed to classes. They would then convert to a class once state was needed. Well, you don't need to do that anymore.
My Most Commonly Used Hooks
The built-in hooks that I use most often are:
- useState
- useReducer
- useEffect
useState
useState
is used to create state properties for your component. It's very similar to this.state
in a class component.
class TodoComponent extends React.Component {
state = {
content: ''
}
...
}
// OR
class TodoComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
content: ''
}
}
...
}
// and what we like
function TodoComponent() {
const [content, setContent] = React.useState('');
...
}
The variable setContent
in the functional component above is the state updater function. It works like this.setState
, and updates the content
state and rerenders the component.
React.useState always returns an array with two items, the state
variable as the first item, and the updater function as the second item. I highly recommend naming the updater function as set<Name of state var>
. This will keep things consistent in your project.
useReducer
useReducer
is kinda like a more powerful useState
. Why use useReducer
?
- You have a lot of state props on your component
- You really like Redux's reducers
If your component has more than one or two state properties, you may prefer to create those props with useReducer
over useState
. It may be easier for you to manage a single dispatch
function that takes a type and payload which will update your components state, than it is to have a bunch of individual state updater functions.
const initialState = {
name: '',
address: '',
city: '',
};
// Just like in Redux
function userReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload,
};
case 'SET_ADDRESS':
return {
...state,
address: action.payload,
};
case 'SET_CITY':
return {
...state,
city: action.payload,
};
}
}
function UserComponent() {
const [state, dispatch] = React.useReducer(userReducer, initialState);
return (
<div>
<h1>{state.name}</h1>
...
</div>
);
}
useEffect
useEffect
handles rerendering of your component based on state or property updates. It is also what you use to handle side effects, aka fetching data from an API.
function UserComponent() {
const [userId, setUserId] = React.useState();
React.useEffect(() => {
async function fetchToken() {
try {
const response = await axios({
method: 'GET',
url: `${API_PATH}/user/${userId}`,
withCredentials: true,
});
setToken(get(response, 'data.trustedTicket'));
} catch (error) {
console.error(error);
}
}
fetchToken();
}, [userId]); // Run the useEffect code when `userId` changes
return (
...
)
}
Custom Hooks
Now that you have more of an understanding on some very common hooks, lets create our own custom hook. First, we need to name the hook.
function useTodos() {}
Please start every hook with the word use
. This is for your own good. The React team has an ESLint plugin that is very helpful at keeping us from messing up our hooks.
We provide an ESLint plugin that enforces rules of Hooks to avoid bugs. It assumes that any function starting with ”use” and a capital letter right after it is a Hook.
Now that we have a hook defined, we can add in some state and functionality.
let nextTodoId = 0;
function useTodos(initialTodos = {}) {
const [todos, setTodos] = React.useState(initialTodos);
const addTodo = content => {
const id = ++nextTodoId;
setTodos({
...todos,
[id]: {
content,
completed: false,
id,
},
});
};
const toggleTodo = id => {
setTodos({
...todos,
[id]: {
content: todos[id].content,
completed: !todos[id].completed,
id,
},
});
};
return [todos, addTodo, toggleTodo];
}
Custom hooks can take parameters just like any other function. Here i'm passing an initialTodos object that will default to an empty object if undefined.
I've add two updater functions addTodo
and toggleTodo
that both update the todos
state property.
I'm returning an array of values, just like the useState
and useReducer
hooks.
...
return [todos, addTodo, toggleTodo];
Using the custom hook
You use the custom useTodos
hook just like any other hook.
function MyComponent() {
const [todos, addTodo, toggleTodo] = useTodos();
return (
<>
<AddTodo addTodo={addTodo}>
<TodoList toggleTodo={toggleTodo} allTodos={todos}>
</>
)
}
We're passing the useTodos
hook values to the and components. When addTodo
is called, for example, will rerender, since we call a state updater function within addTodo
. The todos
object will have updated, and that means the component needs to rerender.
Well I hope this has been helpful for you if you're getting into hooks. Let me know if you have any questions about the above code. Have fun coding 😊
Top comments (6)
Wouldn't it be better to return an object from a custom hook rather than an array?
So instead of returning like this -
We could choose to return -
This way I don't have to worry about the position at which my
todos
or methodaddToDo
is accessible?I don't know if I'd say one way is better than the other. I return arrays sometimes if there aren't a lot of objects being returned, but once I get more than 3 I would probably switch to returning an object. I also use a lot of Typescript, so defining the return type helps with not mixing up variables.
Good article.
Thank you :)
I am kinda confused about the use of useReducer.
Here's an example of using the React.useReducer hook in the
UserComponent
defined in the article:I am using the
useReducer
hook to create state and dispatch variables. In theuserReducer
switch statement i have aSET_NAME
case, which should just update the name property. We do that by dispatching an action of typeSET_NAME
, and with a payload of the new name. This happens in the aboveupdateName
method.Does this make more sense?