loading...

Crazy Idea: Managing React State with Hooks

jsnanigans profile image Brendan Mullins ・3 min read

If you use react you will probably have used some sort of state management like Redux, Zustand, or you might have tried to do everything with React.Context/React.useReducer.

Whichever one I tried, I have found myself wishing for something else. Maybe Recoil will be the solution I long for but it's still in early development.

This has inspired me to try to make my own starting on how I want to use it, and I did not want to create a whole library since I have a history of dropping Open Source projects and it should be small enough to understand in under five minutes.

So I came up with the idea of using custom hooks to share state.

What better way to test this idea than to create a good old Todo list?!

Setup

Let’s start with creating a custom hook:

// stateHook.js
const defaultValue = [{
    id: Date.now(),
    done: false,
    text: "Initial Todo Item"
  }];

// define custom hook
export default function useStateTodo() {
  const [stateList, setStateList] = useState(defaultValue);
  return [stateList];
}

Now you might be thinking: "That's a useless hook", but bear with me - we will change it in a minute.

Now lets add a component that uses this hook:

// list.js
import useStateTodo from "./stateHook";

export default function List() {
  const [todos] = useStateTodo();

  return (
    <ul>
      {todos.map((item) => (
        <li
          key={item.id}
          style={{ textDecoration: item.done ? "line-through" : "none" }}
        >
          <b>{item.text}</b>
        </li>
      ))}
    </ul>
  );
}

This component will render a list of our todo items. For now, there is just the "Initial Todo Item". πŸŽ‰πŸŽ‰

Now let’s add a component to append a new todo list item:

// createTodo.js
export default function CreateTodo() {
  const [val, setVal] = useState("");

  const addTodo = (e) => {
    e.preventDefault()
    setVal("");
  };

  return (
    <form onSubmit={addTodo}>
      <input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
      <button>Add</button>
    </form>
  );
}

This component contains a form with a simple form to create a new todo item. For now though, it does nothing: when you click on "Add" it will just clear the input.

Adding State

Now let's get to the good part, adding state, but I have one requirement, which is to be able to update the state from outside of React. I try to avoid writing business logic in React, so if I can set the state from my API call handlers, I will be a very happy dev.

Now, this will seem weird but stick with me a bit longer, I will add some methods in the hook file to mutate the state:

// stateHook.js
const list = ...

export const listMethods = {
  add: (text) => (list = list.concat([{ id: Date.now(), done: false, text }])),
  update: (id, update) =>
    (list = list.map((li) => {
      if (li.id === id) {
        return {
          ...li,
          ...update
        };
      }
      return li;
    })),
  delete: (id) => (list = list.filter((li) => li.id !== id))
};

And then in the hook, I want to re-implement those methods, but after changing the original list variable, I want to update the state:

// stateHook.js
...

// define custom hook
export default function useStateTodo() {
  const [stateList, setStateList] = useState(list);
  const methods = { ...listMethods };

  useEffect(() => {
    let mounted = true;
    const methods = { ...listMethods };
    Object.keys(methods).forEach((key) => {
      listMethods[key] = (...params) => {
        methods[key](...params);
        if (mounted) {
          setStateList([...list]);
        }
      };
    });
    return () => {
      mounted = false;
    };
  }, []);

  return [stateList, listMethods];
}

Now when I call listMethods.add(newItem), not only will the new item be added to the state but also setStateList will be called with the new state.

Now let us hook up the createTodo component with this state; notice that I don't use the hook we created, only listMethods is imported. This has the advantage that it will never re-render when the store changes.

// createTodo.js
export default function CreateTodo() {
  const [val, setVal] = useState("");

  const addTodo = (e) => {
    e.preventDefault();
    val && listMethods.add(val);
    setVal("");
  };

  return (
    <form onSubmit={addTodo}>
      <input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
      <button>Add</button>
    </form>
  );
}

Last step I want is to add a "delete" and "check done" features to the list:

// list.js
export default function List() {
  const [todos, todosMethods] = useStateTodo();

  const toggleDone = (item) => {
    todosMethods.update(item.id, { done: !item.done });
  };

  const deleteItem = (item) => {
    todosMethods.delete(item.id);
  };

  return (
    <ul>
      {todos.map((item) => (
        <li
          key={item.id}
          style={{ textDecoration: item.done ? "line-through" : "none" }}
        >
          <button onClick={() => deleteItem(item)}>
            <span role="img" aria-label={`delete ${item.text}`}>
              πŸ—‘οΈ
            </span>
          </button>
          <input
            type="checkbox"
            checked={item.done}
            onChange={() => toggleDone(item)}
          />{" "}
          <b>{item.text}</b>
        </li>
      ))}
    </ul>
  );
}

Here is the result:
https://codesandbox.io/s/state-with-hooks-1-3nu38

That's it! Hope you found this interesting.

I'm pretty sure this is not a good idea but I would love to hear your opinion on this.

Discussion

pic
Editor guide