HashSet: Immutable data structure

Imagine we want to add a checkbox for each post. The ideal data structure for this is a Set. With a Set we can easily store, update, and retrieve the id of the posts that are checked.

Problem is that Set is a mutable data structure, so it doesn't work well with hooks like useState that require immutable updates.

That's where effect shines also on the frontend, by providing immutable, performant, and stack safe data structures.

Let's see an example.

HashSet: Immutable Set data structure

Let's start by adding the checkbox to each post:

const SinglePost = ({
  post,
  onChecked,
  checked,
}: {
  post: typeof Post.Encoded;
  checked: boolean;
  onChecked: () => void;
}) => {
  const [_, setLiked, pending] = useActionState(() => likePost(post.id), null);
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <button disabled={pending} onClick={setLiked}>
        Like
      </button>
      <input type="checkbox" checked={checked} onChange={onChecked} />
    </div>
  );
};

Inside Posts we add useState to store the checked state for posts. The state uses the HashSet data structure provided by effect:

import { HashSet } from "effect";

export default function Posts({
  posts,
}: {
  posts: readonly (typeof Post.Encoded)[];
}) {
  const [checkedPosts, setCheckedPosts] = useState(HashSet.empty<number>());
  return (
    <div>
      {posts.map((post) => (
        <SinglePost key={post.id} post={post} />
      ))}
    </div>
  );
}

HashSet is mostly equivalent to Set, but it's immutable.

For the checked state we use HashSet.has to check if the post is checked:

Data structures in effect don't provide dot-methods like a class, so you cannot call checkedPosts.has(post.id).

Instead, they provide functions that operate on the data structure: you pass the data structure as the first argument (HashSet.has(checkedPosts, post.id)).

This allows for better composability and tree shaking.

export default function Posts({
  posts,
}: {
  posts: readonly (typeof Post.Encoded)[];
}) {
  const [checkedPosts, setCheckedPosts] = useState(HashSet.empty<number>());
  return (
    <div>
      {posts.map((post) => (
        <SinglePost
          key={post.id}
          post={post}
          checked={HashSet.has(checkedPosts, post.id)}
        />
      ))}
    </div>
  );
}

For onChecked instead HashSet provides a convenient HashSet.toggle function that removes id if it's present, or adds it otherwise:

Since HashSet is immutable, HashSet.toggle returns a new HashSet with the updated state. We can therefore simply call setCheckedPosts with the new state.

export default function Posts({
  posts,
}: {
  posts: readonly (typeof Post.Encoded)[];
}) {
  const [checkedPosts, setCheckedPosts] = useState(HashSet.empty<number>());
  return (
    <div>
      {posts.map((post) => (
        <SinglePost
          key={post.id}
          post={post}
          checked={HashSet.has(checkedPosts, post.id)}
          onChecked={() =>
            setCheckedPosts(HashSet.toggle(checkedPosts, post.id))
          }
        />
      ))}
    </div>
  );
}

This is all you need to implement checking and unchecking posts. The data structure makes sure that each id is unique inside the set, so everything works as expected.

This was just a simple example. Effect provides many more immutable data structures that can be combined to manage all kind of complex state:

  • HashSet
  • HashMap
  • Trie
  • RedBlackTree
  • SortedSet
  • SortedMap

Using these will solve many problems related to state management in React.