npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

qumu

v0.0.2

Published

Hooks for async query and mutation

Downloads

13

Readme

QUMU

Hooks for async query and mutation

Installation

$ npm i qumu --save
# or
$ yarn add qumu

Quick Start

import React from "react";
import { render } from "react-dom";
import { useQuery, createProvider } from "qumu";
import { GetPostsApi, AddPostApi } from "../api";

// creating the qumu query provider
const Provider = createProvider();

const App = () => {
  // getPosts is a function that use to execute the GetPostsApi
  const getPosts = useQuery(GetPostsApi);
  const addPost = async () => {
    // wait until AddPostApi is done and refetch the getPosts query
    await AddPostApi({
      id: Math.random(),
      userId: 1,
      title: "New Post",
      body: "New Post",
    });
    getPosts.refetch();
  };
  // the query result provides loading and data props
  const { loading, data } = getPosts();
  return (
    <>
      <div>
        {loading ? "Loading..." : <xmp>{JSON.stringify(data, null, 2)}</xmp>}
      </div>
      <button onClick={addPost}>Add Post</button>
    </>
  );
};

render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById("root")
);

Examples

Counter App

You can use mutation to modify query cached data

import { useQuery, useMutation } from "qumu";

const CountQuery = () => 0;
// execute query and get data
const useCount = () => useQuery(CountQuery).call().data;
const useIncrease = () =>
  // the first argument is which query need to be modified
  // the second argument is mutation that retrieves the query object
  useMutation(CountQuery, (countQuery) => countQuery.data++);

const App = () => {
  const count = useCount();
  const increase = useIncrease();

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => increase()}>Increase</button>
    </div>
  );
};

Do we need another state management library for the global state?

Optimistic Update

import { useQuery, useMutation } from "qumu";
import { GetPostsApi, AddPostApi } from "../api";

const App = () => {
  const getPosts = useQuery(GetPostsApi);
  const addPost = useMutation(GetPostApi, async (getPostsQuery) => {
    const post = {
      id: Math.random(),
      userId: 1,
      title: "New Post",
      body: "New Post",
    };

    AddPostApi(post);

    getPostsQuery.data = [...getPostsQuery.data, post];
  });
  // execute query to get the result for rendering
  // the query result provides loading and data props
  const { loading, data } = getPosts();
  return (
    <>
      <div>
        {loading ? "Loading..." : <xmp>{JSON.stringify(data, null, 2)}</xmp>}
      </div>
      <button onClick={addPost} disabled={loading}>
        Add Post
      </button>
    </>
  );
};

Keyed Query

import { useQuery, useMutation } from "qumu";

// query key is string if no query key specified, qumu uses function name as query key
const useCount = () => useQuery("count", () => 0).call().data;
const useIncrease = () =>
  useMutation("count", (countQuery) => countQuery.data++);

const App = () => {
  const count = useCount();
  const increase = useIncrease();

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => increase()}>Increase</button>
    </div>
  );
};

Passing payload to query and mutation

import { useQuery, useMutation } from "qumu";

const CountQuery = ({ value }) => value;
// execute query and get data
const useCount = () => useQuery(CountQuery, { value }).call();
const useIncrease = () =>
  // the first argument is which query need to be modified
  // the second argument is mutation that retrieves the query object
  useMutation(CountQuery, (countQuery, { value = 1 }) => {
    countQuery.data += value;
  });

const App = () => {
  const count = useCount();
  const increase = useIncrease();

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => increase()}>+1</button>
      <button onClick={() => increase({ value: 2 })}>+2</button>
    </div>
  );
};

Handling async mutation

const useAsyncAction = () =>
  useMutation(async (_, payload) => {
    // delay in 1s
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return payload;
  });

const App = () => {
  const asyncAction = useAsyncAction();
  // retrieve mutation result. The result provides: loading, data, error props
  // These props are useful for handling async mutating status
  const { loading, data, error } = asyncAction.result;
  return (
    <>
      {loading && "Processing..."}
      Data: {data}
      Error: {error}
      <button onClick={() => asyncAction(Math.random())}>Async Action</button>
    </>
  );
};

Real World Example: Todo App

What you will learn?

  • Using qumu for storing global state: editing todo, search term, current filter
  • Using useResolver to resolve multiple query results
  • Using ErrorBoundary to handle async error and retry async data fetching
  • Using Suspense to display loading indicator
  • Using memoized resolver to improve performance
  • Optimistic add/update/remove
import React, { Suspense, useEffect, useRef } from "react";
import { render } from "react-dom";
import { ErrorBoundary } from "react-error-boundary";
import {
  useQuery,
  useMutation,
  useResolver,
  createProvider,
  memoize,
} from "qumu";
import axios from "axios";

const api = (url, method = "get", data) =>
  axios({
    method,
    url: `https://sq2rp.sse.codesandbox.io/qumu-todo${url}`,
    data,
    params: {
      // the success response will be delayed in 500ms
      delay: 500,
      // 50% of requests will be failed, that will cause an client errors and we use ErrorBoundary to handle these errors
      fail: 0.5,
    },
  }).then((res) => res.data);

const Provider = createProvider({
  // support suspense for loading query data
  suspense: true,
});

const FilterQuery = () => "all";

const TermQuery = () => "";

const SelectedTodoQuery = () => null;

const TodoListQuery = () => api("/", "get");

const TodoSorter = (a, b) =>
  a.title > b.title ? 1 : a.title < b.title ? -1 : 0;

const FilteredTodoResolver = memoize((todoList, filter, term) => {
  // if search term presents
  if (term) {
    term = term.toLowerCase();
    return todoList
      .filter((todo) => (todo.title || "").toLowerCase().indexOf(term) !== -1)
      .sort(TodoSorter);
  }
  // do filtering
  return filter === "all"
    ? todoList.slice().sort(TodoSorter)
    : filter === "active"
    ? todoList.filter((todo) => !todo.completed).sort(TodoSorter)
    : todoList.filter((todo) => todo.completed).sort(TodoSorter);
});
const TodoSummaryResolver = memoize({
  all: (todoList) => todoList.length,
  active: (todoList) => todoList.filter((todo) => !todo.completed).length,
  completed: (todoList) => todoList.filter((todo) => todo.completed).length,
});

const useFilteredTodoList = () => {
  const filterResult = useFilter();
  const termResult = useTerm();
  const todoListResult = useTodoList();

  return useResolver(
    [todoListResult, filterResult, termResult],
    FilteredTodoResolver
  );
};

const useTodoSummary = () => {
  const todoListResult = useTodoList();
  return useResolver(todoListResult, TodoSummaryResolver);
};

const useSelectedTodo = () => useQuery(SelectedTodoQuery).call();

const useTodoList = () => useQuery(TodoListQuery).call();

const useFilter = () => useQuery(FilterQuery).call();

const useTerm = () => useQuery(TermQuery).call();

const useUpdateFilter = () =>
  useMutation(
    FilterQuery,
    (filterQuery, { filter }) => (filterQuery.data = filter)
  );
const useUpdateTerm = () =>
  useMutation(TermQuery, (termQuery, { term }) => (termQuery.data = term));

const useUpdateSelectedTodo = () =>
  useMutation(
    SelectedTodoQuery,
    (selectedTodoQuery, { todo }) => (selectedTodoQuery.data = todo)
  );

const useAddTodo = () =>
  useMutation(TodoListQuery, (todoListQuery, { data }) => {
    // generate random id for new todo
    // add $local- prefix for local id
    const todo = { ...data, id: "$local-" + Math.random().toString(36) };
    // optimistic update
    todoListQuery.data = [...todoListQuery.data, todo];
    api(`/`, "post", todo)
      .then(({ id }) => {
        // replace local id with server id
        todoListQuery.data = todoListQuery.data.map((x) =>
          x === todo ? { ...x, id } : x
        );
      })
      .catch((error) => {
        todoListQuery.data = todoListQuery.data.filter((x) => x !== todo);
        alert(error.message);
      });
  });

const useUpdateTodo = () =>
  useMutation(TodoListQuery, (todoListQuery, { data }) => {
    const prevTodo = todoListQuery.data.find((x) => x.id === data.id);
    todoListQuery.data = todoListQuery.data.map((todo) =>
      todo.id === data.id ? { ...todo, ...data } : todo
    );
    api(`/${data.id}`, "put", data)
      // restore previous todo data if error
      .catch((error) => {
        if (
          todoListQuery.data.findIndex((todo) => todo.id === data.id) !== -1
        ) {
          todoListQuery.data = todoListQuery.data.map((x) =>
            x.id === data.id ? prevTodo : x
          );
        }
        alert(error.message);
      });
  });

const useRemoveTodo = () =>
  useMutation(
    [TodoListQuery, SelectedTodoQuery],
    ([todoListQuery, selectedTodoQuery], { id }) => {
      // store removedTodo for later use
      const removedTodo = todoListQuery.data.find((x) => x.id === id);
      // filter our the removed todo from the list
      todoListQuery.data = todoListQuery.data.filter((x) => x.id !== id);
      // clear edit form if we are removing a editing todo
      if (selectedTodoQuery.data && selectedTodoQuery.data.id === id) {
        selectedTodoQuery.data = null;
      }
      api(`/${id}`, "delete")
        // re-added removedTodo if error
        .catch((error) => {
          todoListQuery.data = [...todoListQuery.data, removedTodo];
          alert(error.message);
        });
    }
  );

const FilterItem = ({ type, text, unchecked, num }) => {
  const filter = useFilter().data;
  const updateFilter = useUpdateFilter();
  const updateTerm = useUpdateTerm();
  const handleClick = () => {
    updateFilter({ filter: type });
    updateTerm({ term: "" });
  };

  return (
    <label>
      <input
        type="radio"
        checked={unchecked ? false : filter === type}
        readOnly={true}
        onClick={handleClick}
      />{" "}
      {text} ({num})
    </label>
  );
};

const FilterBar = () => {
  const term = useTerm().data;
  const todoSummary = useTodoSummary().data;
  const updateTerm = useUpdateTerm();
  const filteredTodoList = useFilteredTodoList();

  const handleTermChange = (e) => {
    updateTerm({ term: e.target.value });
  };

  return (
    <p>
      <FilterItem
        text="All"
        unchecked={term}
        type="all"
        num={todoSummary.all}
      />{" "}
      <FilterItem
        text="Completed"
        unchecked={term}
        type="completed"
        num={todoSummary.completed}
      />{" "}
      <FilterItem
        text="Active"
        unchecked={term}
        type="active"
        num={todoSummary.active}
      />{" "}
      <input
        placeholder="Enter todo title"
        value={term}
        onChange={handleTermChange}
      />
      {term && <> Found {filteredTodoList.data.length}</>}
    </p>
  );
};

const TodoForm = () => {
  const titleRef = useRef();
  const completedRef = useRef();
  const selectedTodo = useSelectedTodo().data;
  const updateSelectedTodo = useUpdateSelectedTodo();
  const addTodo = useAddTodo();
  const updateTodo = useUpdateTodo();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (selectedTodo) {
      updateTodo({
        data: {
          id: selectedTodo.id,
          title: titleRef.current.value,
          completed: completedRef.current.checked,
        },
      });
      // clear selected todo
      updateSelectedTodo({ todo: null });
    } else {
      addTodo({
        data: {
          title: titleRef.current.value,
          completed: completedRef.current.checked,
        },
      });
      // clear inputs
      completedRef.current.checked = false;
      titleRef.current.value = "";
    }
  };

  useEffect(() => {
    titleRef.current.value = selectedTodo ? selectedTodo.title : "";
    completedRef.current.checked = selectedTodo
      ? selectedTodo.completed
      : false;
  }, [selectedTodo]);

  return (
    <form onSubmit={handleSubmit}>
      <p>
        <input type="text" ref={titleRef} placeholder="What need to be done?" />
      </p>
      <p>
        <label>
          <input type="checkbox" ref={completedRef} /> Completed
        </label>
      </p>

      <p>
        <button type="submit">{selectedTodo ? "Save" : "Add"}</button>
        {selectedTodo && (
          <button onClick={() => updateSelectedTodo({ todo: null })}>
            Cancel
          </button>
        )}
      </p>
    </form>
  );
};

const TodoList = () => {
  const { data, loading } = useFilteredTodoList();
  const updateSelectedTodo = useUpdateSelectedTodo();
  const removeTodo = useRemoveTodo();

  if (loading) return <div>Loading...</div>;
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>
          <span
            style={{ textDecoration: todo.completed ? "line-through" : "none" }}
          >
            {todo.id}: {todo.title}
          </span>{" "}
          <button onClick={() => updateSelectedTodo({ todo })}>edit</button>{" "}
          <button onClick={() => removeTodo({ id: todo.id })}>remove</button>{" "}
        </li>
      ))}
    </ul>
  );
};

const ErrorFallback = ({ error, resetErrorBoundary }) => {
  return (
    <div>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
};

const App = () => {
  const { refetch } = useTodoList();
  return (
    /* refetch TodoList if there is any error */
    <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => refetch()}>
      <TodoForm />
      <FilterBar />
      <TodoList />
    </ErrorBoundary>
  );
};

render(
  <Provider>
    <Suspense fallback="Loading...">
      <App />
    </Suspense>
  </Provider>,
  document.getElementById("root")
);