@akoskovacs/keyed-list

Immutable keyed list collection

Usage no npm install needed!

<script type="module">
  import akoskovacsKeyedList from 'https://cdn.skypack.dev/@akoskovacs/keyed-list';
</script>

README

keyed-list.ts

Efficiently store and retreive list elements based on their id key. This approach has the same benefits as having both an array and a hash for the same list.

With the array we can simply iterate through the elements or use them by their index. And with the hash, we can efficiently retrieve them, just by the id. Although, this implementation will only store the ids in the array to conserve space. But, this mechanism is completely transparent, the user don't have to think about it.

The immutability makes this library easy to use with React or other functional libraries and is also less error-prone.

Installation

The preferred way is to add it to the project locally as a dependency:

npm install --save @akoskovacs/keyed-list

Or via yarn:

yarn add @akoskovacs/keyed-list

Usage

The list element must include a unqiue id property. The id can be a GUID given by an API. It then can be used to efficiently address the elements. Take this example:

import keyedList from '@akoskovacs/keyed-list';

interface PostItem {
  id: string;
  author: string;
  content: string;
  createdAt: Date;
};

const posts: PostItem[] = [
  {
    id: '1234',
    author: 'John',
    votes: 4,
    content: 'Hello, world!',
    createdAt: new Date('2020-12-11 10:45:21')
  },
  {
    id: '2222',
    author: 'Steve',
    votes: 0,
    content: 'Second post',
    createdAt: new Date('2020-12-11 14:12:55')
  },
  {
    id: '4242',
    author: 'Peter',
    votes: 2,
    content: 'This is not the end',
    createdAt: new Date('2020-12-11 14:12:55')
  }
];

After the API gave back the elements the library can start manipulating the data, with the following functions:

Function name Description Example
fromArray Create a list from an array const list = keyedList.fromArray(posts);
toArray Converts the list back into an array const newPosts = keyedList.toArray(list);
getIds Retrieve multiple elements by their ids const ids = keyedList.getIds(list);
getById Retrieve the element by its id const steve = keyedList.getById(list, '2222');
getByIds Converts the list back into an array const sp = keyedList.getByIds(list, ['4242, '2222']);
getIdByIndex Retrieve the id by the element's numeric index const firstId = keyedList.getIdByIndex(list, 0);
getFirst Gets the first element in the list const first = keyedList.getFirst(list);
getLast Gets the last elemenet in the list const last = keyedList.getLast(list);
getByIndex Gets the element by its index const second = keyedList.getByIndex(list, 1);
getCount Get the length of the list const count = keyedList.getCount(list);
update Update properties of given list element const newList = keyedList.update(list, { id: '4242', votes: 1 });
append Adds a new element to the end the list const newList = keyedList.append(list, { id: '5200', author: 'Riley' /* ... */});
insert Insert a new element to the beginning of the list const newList = keyedList.insert(list, { id: '5200', author: 'Riley' /* ... */});
removeById Removes an element by its id const newList = keyedList.removeById(list, '2222');
map Iterates through the list elements const nameArray = keyedList.map(list, p => p.author);
mapIds Iterates through the ids of the list const ids = keyedList.mapIds(list, id => id);
filter Filters out elements which stasify a given condition const nonZeroVotes = keyedList.filter(list, p => p.votes > 0);
sort Sorts the array, by a given comparison function const sortedList = keyedList.sort(list, (left, right) => left.votes - right.votes);

A more complete example with React

Posts.tsx:

/* ... API handlers ...*/

type PostItemList = keyedList.IdKeyedList<PostItem>;

const Posts = () => {
  const [posts, setPosts] = React.useState<PostItemList>(keyedList.fromArray());
  const [isInitialized, setIsInitialized] = React.useState<boolean>(false);

  React.useEffect(() => {
    if (!isInitialized) {
      if (!initialPosts) {
        fetchAllPosts();
      }
      setIsInitialized(true);
    } else {
      fetchAllPosts();
    }
  }, []);

  const fetchAllPosts = () => {
    // fetchPosts does a GET request to load all posts into an array, then those are converted into a list
    fetchPosts(cs => setPosts(keyedList.fromArray(cs)));
  };

  const submitPost = () => {
    // savePost does the POST request for creating a new post, the new item then can be inserted to the list
    savePost(content, (c) => {
      setPosts(keyedList.insert(posts, c));
      setContent("");
    });
    return true;
  };

  const setPostContent = (id: string, content: string) => {
    // When the content is edited, the new content is updated through this call
    setPosts(keyedList.update(posts, { id, content }));
    return true;
  };

  const voteOnPost = (id: string) => {
    // When the post is voted on, the request will give back the number of all votes, so we can show it to the users
    votePost(id, (votes) => {
      setPosts(keyedList.update(posts, { id, vote: votes }));
    });
  };

  const doDelete = (id: string) => {
    if (confirm('Are you sure to delete this post?')) {
      setPosts(keyedList.removeById(posts, id));
    }
  };

  return (
    <>
      {
        keyedList.mapIds(id =>
          <Post
            id={ id }
            key={ id }
            editContent={ setPostContent }
            getById={ id => keyedList.getById(posts, id) }
            onVote={ id => voteOnPost }
            onDelete={ doDelete } />
        )
      }
    </>
  );
};

export default Posts;

Post.tsx:

interface PostProps {
  id: Uuid;
  onEditContent?: (id: string, content: string) => void;
  onVote?: (id: string) => void;
  onDelete?: (id: string) => void;
  getById?: (id: string) => PostItem;
}


export const Post = ({ id, getById, setContent, onDelete }: PostProps) => {
  const post = (id && getById) ? getById(id) : undefined;

  if (!post)
    return undefined;

  return (
    <div className="post">
      <p><em>Posted by { post.author }</em></p>
      {
        onVote &&
          <button className="vote" onClick={ () => onVote(id) } />
      }
      <p className="post-content">
        { post.content }
      </p>
      {
        onEditContent &&
        /* create and editor for the content and call onEditContent when done */
      }
      {
        onDelete &&
          <button className="delete" onClick={ () => onDelete(id) } />
      }
    </div>
  );
};