aposto

A local state management for Apollo GraphQL

Usage no npm install needed!

<script type="module">
  import aposto from 'https://cdn.skypack.dev/aposto';
</script>

README

aposto

A local state management for Apollo GraphQL

Installation

Using npm

    npm i aposto --save

Using yarn

    yarn add aposto

Counter App without GraphQL server

import React from "react";
import { render } from "react-dom";
import { createStore, Provider, useQuery, useAction } from "aposto";

const COUNT_STATE = "count";
const INCREASE_ACTION = "increase";
const INCREASE_ASYNC_ACTION = "increase-async";

// this saga will be triggered in initializing phase of the store
function* InitSaga({ when }) {
  // wait for INCREASE_ACTION and then call OnIncreaseSaga
  yield when(INCREASE_ACTION, OnIncreaseSaga);
  // wait for INCREASE_ACTION and then call OnIncreaseSaga with specified payload
  yield when(INCREASE_ASYNC_ACTION, OnIncreaseSaga, {
    payload: { async: true },
  });
}

function* OnIncreaseSaga({ merge, delay }, { async }) {
  if (async) {
    // delay in 1s then do next action
    yield delay(1000);
  }
  // merge specified piece of state to the whole state
  yield merge({
    // can pass state value or state reducer which retrieves previous state value as first param and return next state
    [COUNT_STATE]: (prev) => prev + 1,
  });
}

const store = createStore({
  // set initial state for the store
  state: { [COUNT_STATE]: 0 },
  init: InitSaga,
});

const App = () => {
  // select COUNT_STATE from the store state
  const count = useQuery(COUNT_STATE);
  // retrieve action disspatchers
  const [increase, increaseAsync] = useAction(
    INCREASE_ACTION,
    INCREASE_ASYNC_ACTION
  );

  return (
    <>
      <h1>{count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={increaseAsync}>Increase Async</button>
    </>
  );
};

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Todo App with GraphQL server

import { render } from "react-dom";
import { createStore, Provider, gql, useQuery, useAction } from "aposto";
import { Suspense, useEffect, useRef, useState } from "react";

const store = createStore({
    uri: "https://6dsu8.sse.codesandbox.io/",
});

const FETCH_ALL_TODO_PROPS = `
  id
  title
  description
  createdOn
  updatedOn
  completed
`;
const GET_TODOS = gql`
  query GetTodos {
    todos { ${FETCH_ALL_TODO_PROPS} }
  }
`;

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: ID!, $title: String, $description: String) {
    update(id: $id, title: $title, description: $description) {
      ${FETCH_ALL_TODO_PROPS}
    }
  }
`;

const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: ID!) {
    toggle(id: $id) { ${FETCH_ALL_TODO_PROPS} }
  }
`;

const ADD_TODO = gql`
  mutation AddTodo {
    add { ${FETCH_ALL_TODO_PROPS} }
  }
`;

const REMOVE_TODO = gql`
  mutation RemoveTodo($id: ID!) {
    remove(id: $id) { ${FETCH_ALL_TODO_PROPS} }
  }
`;

const App = () => {
    const titleInputRef = useRef();
    const pageSize = 10;
    const [page, setPage] = useState(0);
    const descriptionInputRef = useRef();
    const [editingTodo, setEditingTodo] = useState(null);
    // queries
    const { todos, $refetch } = useQuery(GET_TODOS);
    // mutations
    const [add, { $loading: adding }] = useAction(ADD_TODO);
    const [update, { $loading: updating }] = useAction(UPDATE_TODO);
    const [toggle, { $loading: toggling }] = useAction(TOGGLE_TODO);
    const [remove, { $loading: removing }] = useAction(REMOVE_TODO);
    const loading = adding || updating || removing || toggling;

    const handleEdit = (id) => {
        setEditingTodo(id);
    };

    const handleRemove = (id) => {
        // remove the todo object from the cache after mutating
        remove({ id }, { removeFromCache: () => ({ Todo: id }) });
    };

    const handleAdd = () => {
        add({}, { update: () => $refetch() });
    };

    const handleSave = () => {
        setEditingTodo(null);
        update({
            id: editingTodo,
            title: titleInputRef.current.value,
            description: descriptionInputRef.current.value,
        });
    };

    useEffect(() => {
        // adjust current page if it is greater than total pages
        if (page * pageSize > todos.length) {
            // move to last page
            setPage(Math.ceil(todos.length / pageSize));
        }
    }, [page, pageSize, todos.length]);

    return (
        <>
            <div>
                <button disabled={loading} onClick={handleAdd}>
                    Add
                </button>{" "}
                ({todos.length} todos){" "}
                {new Array(Math.ceil(todos.length / pageSize))
                    .fill(null)
                    .map((_, index) => (
                        <button
                            key={index}
                            style={{ fontWeight: index === page ? "bold" : "normal" }}
                            onClick={() => setPage(index)}
                        >
                            {index + 1}
                        </button>
                    ))}{" "}
                {loading && <strong style={{ color: "red" }}>Updating... </strong>}
            </div>
            <div>
                {todos.slice(page * pageSize, (page + 1) * pageSize).map((todo) => {
                    const isEditing = editingTodo === todo.id;
                    return (
                        <div key={todo.id} style={{ opacity: todo.completed ? 0.5 : 1 }}>
                            <hr />
                            <div>
                                {isEditing ? (
                                    <>
                                        <button onClick={() => handleSave()}>Save</button>
                                        <button onClick={() => handleEdit(null)}>Cancel</button>
                                    </>
                                ) : (
                                    <>
                                        <button
                                            disabled={loading}
                                            onClick={() => handleEdit(todo.id)}
                                        >
                                            Edit
                                        </button>
                                        <button
                                            disabled={loading}
                                            onClick={() => toggle({ id: todo.id })}
                                        >
                                            Toggle
                                        </button>
                                        <button
                                            disabled={loading}
                                            onClick={() => handleRemove(todo.id)}
                                        >
                                            Remove
                                        </button>
                                    </>
                                )}{" "}
                                {isEditing ? (
                                    <input ref={titleInputRef} defaultValue={todo.title} />
                                ) : (
                                    <strong>
                                        {todo.title} ({todo.updatedOn})
                                    </strong>
                                )}
                            </div>
                            <p>
                                {isEditing ? (
                                    <textarea
                                        ref={descriptionInputRef}
                                        defaultValue={todo.description}
                                        rows={5}
                                        style={{ width: "100%" }}
                                    />
                                ) : (
                                    <span>{todo.description}</span>
                                )}
                            </p>
                        </div>
                    );
                })}
            </div>
        </>
    );
};

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