DEV Community

marques woodson
marques woodson

Posted on • Edited on • Originally published at marqueswoodson.com

Write Custom Hooks in React

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.

Carleton Dance

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)

Collapse
 
pritampatil profile image
Pritam Patil

Wouldn't it be better to return an object from a custom hook rather than an array?

So instead of returning like this -

return [todos, addTodo, toggleTodo]
Enter fullscreen mode Exit fullscreen mode

We could choose to return -

return  { todos, addTodo, toggleTodo }
Enter fullscreen mode Exit fullscreen mode

This way I don't have to worry about the position at which my todos or method addToDo is accessible?

Collapse
 
mwoodson profile image
marques woodson

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.

Collapse
 
bernardbaker profile image
Bernard Baker

Good article.

Collapse
 
mwoodson profile image
marques woodson

Thank you :)

Collapse
 
monfernape profile image
Usman Khalil

I am kinda confused about the use of useReducer.

Collapse
 
mwoodson profile image
marques woodson

Here's an example of using the React.useReducer hook in the UserComponent defined in the article:

function UserComponent() {
  // userReducer is defined above in the article.
  const [state, dispatch] = React.useReducer(userReducer, initialState);

  const [input, setInput] = React.useState('');

  const updateInput = (input: string) => {
    setInput(input);
  };

  const updateName = () => {
    dispatch({
      type: 'SET_NAME',
      payload: input,
    });
  };

  return (
    <div>
      <h1>My name is: {state.name}</h1>
      <input value={input} onChange={e => updateInput(e.target.value)} placeholder="Enter name here" />
      <button onClick={updateName}>Submit</button>
      ...
    </div>
  );
}

I am using the useReducer hook to create state and dispatch variables. In the userReducer switch statement i have a SET_NAME case, which should just update the name property. We do that by dispatching an action of type SET_NAME, and with a payload of the new name. This happens in the above updateName method.

Does this make more sense?