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 toSet
, 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 newHashSet
with the updated state. We can therefore simply callsetCheckedPosts
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.