react-use-database

1.0.1 • Public • Published

react-use-database

relational data at its simplest

react-use-database gives you an opinionated interface, efficient data flow, and concise global state management. It forces you to think about your client-side data in the context of a queryable database. It gives you two global data stores: an entity store and a query store.

The entity store contains all of your model data. It’s just a big json blob that is the source of truth for any database entity that you have defined via Normalizr’s notion of schemas (eg UserSchema, TodoSchema, CommentSchema, etc.). Note, Normalizr is a peer dependency of react-use-database, you should familiarize yourself with it.

But, what good is a database without a way to pull data from it? That’s where the query store comes in. A query is comprised of a schema and a value. The query {schema: UserSchema, value: 32} would return you a user whose id is 32. The query {schema: [UserSchema], value: [32, 33, 34]} would return you an array of three users. By tracking and updating your entities and queries in their respective stores, every component can have access to the latest data with almost no additional code.

I used to implement relational state management with Redux/Normalizr. It worked well, but there was too much boilerplate and trying to onboard a new-hire to the idea was daunting. Redux and Normalizr are an incredible pairing, Dan Abramov was really onto something with his creation of each (He’s even done an Egghead tutorial series in which he describes almost the exact design pattern I’ve implemented in this library). The problem with Redux is that it lacks opinionation and it can be overwhelmingly verbose. It’s vulnerable to anti-patterns and there’s nothing inherent to its API to enforce its proper use. When used improperly, redux can become more of a burden than an asset.

NPM JavaScript Style Guide

Install

npm install --save react-use-database

Demo

Live Demo: https://malerba118.github.io/react-use-database/
Demo Code: https://github.com/malerba118/react-use-database/tree/master/example/src

Simplest Usage

import React from "react";
import ReactDOM from "react-dom";
import { schema } from "normalizr";
import createDB from "react-use-database";
 
const TodoSchema = new schema.Entity("Todo");
 
let [DatabaseProvider, useDB] = createDB([TodoSchema], {
  // Seed the database
  defaultEntities: {
    Todo: {
      1: {
        id: 1,
        text: "Buy cheese"
      }
    }
  }
});
 
const queries = {
  getTodoById: id => {
    return {
      schema: TodoSchema,
      value: id
    };
  }
};
 
const App = props => {
  let db = useDB();
  let queryToGetTodoWithIdOne = queries.getTodoById(1);
  let todo = db.executeQuery(queryToGetTodoWithIdOne);
  return <span>{todo.text}</span>;
};
 
ReactDOM.render(
  <DatabaseProvider>
    <App />
  </DatabaseProvider>,
  document.getElementById("root")
);

Complex Usage

See the live example and example code for a complex implementation

Creating the Database

See the code

Once we wrap our app in the DatabaseProvider we’re good to go. We can now use our database hook in any component to query the database.

Fetching Todos

See the code

Boom, now you can fetchTodos from anywhere and your TodosComponent will re-render with the latest list of todos.

Updating a Todo

See the code

This has to be my favorite one. You don’t even need to update any queries. You just take the updated Todo from the response body, normalize it, and deep merge it into the entity store and then your TodosComponent re-renders with the updated data. Virtually zero effort involved.

Creating a Todo

See the code

You might be seeing a pattern here. Updating the database is almost always as simple as normalizing data and passing the entities object to mergeEntities. This deep merges the entities patch onto the existing entities object. Once our Todo is created, we need to add its id to our ALL_TODOS query. Once this is done, our TodosComponent will re-render with the new todo.

Deleting a Todo

See the code

To delete a todo, we can add a soft delete indicator to the todo in the database and we can update any relevant queries to omit the deleted todo id. Here’s a case where we probably don’t want to just normalize the data from the response and call mergeEntities on it.

Optimistic Updates

See the code

We also can easily perform optimistic updates by normalizing and merging entities before the API call has finished. And then if the API call comes back with an error level status code, we can merge the original todo back into entities to revert the update.

Other Queries

See the code

Because a query is just a schema and value, we can create our own queries whose state is not tracked by the query store. For example, if we received a todo id as a url parameter, we could do something like the above.

API

createDB(entitySchemas, options)

Creates DatabaseProvider and useDB hook.

  • entitySchemas: required Array or object whose values are normalizr Entity schemas
  • options: optional options
    • storedQueryDefinitions: Object whose keys are query names and whose values have form { schema, defaultValue }
    • defaultEntities : An entities object to seed the database

Usage

let [ DatabaseProvider, useDB ] = createDB(
  models,
  {
    storedQueryDefinitions: {
      ALL_TODOS: {
        schema: [models.TodoSchema],
        defaultValue: []
      },
      ACTIVE_TODOS: {
        schema: [models.TodoSchema],
        defaultValue: []
      },
      COMPLETED_TODOS: {
        schema: [models.TodoSchema],
        defaultValue: []
      },
    },
    defaultEntities: {
      Todo: {
        1: {
          id: 1,
          text: 'Buy cheese',
          completed: false
        }
      }
    }
  }
);

DatabaseProvider

React context provider that enables react-use-database to have global state.

Usage

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

useDB()

React database hook that allows you to query and update the database

Usage

const useNormalizedApi = () => {
  let db = useDB();
 
  return {
    ...
    addTodo: async (text) => {
      let todo = await api.addTodo(text);
      let { result, entities } = normalize(
        todo,
        apiSchemas.addTodoResponseSchema
      );
      db.mergeEntities(entities); // Merge new todo data into database
      db.updateStoredQuery('ALL_TODOS', (prevArray) => [...prevArray, todo.id]);
    },
    ...
  };
};
 
const TodosComponent = (props) => {
  let db = useDB();
 
  let allTodosQuery = db.getStoredQuery('ALL_TODOS');
  let todos = db.executeQuery(allTodosQuery);
 
  return (
    <JSON data={todos} />
  )
}

mergeEntities(entitiesPatch, options)

Immutably deep merges an entities patch on to the current entities object to produce next entities state.

  • entitiesPatch: required function or partial entities object. If function, one argument will be passed, the current entities. Under the hood, lodash's mergeWith is called to merge the entitiesPatch onto the current entities to produce the next enitities object.
  • options: optional options

Usage

const TodosComponent = (props) => {
  let db = useDB();
 
  let todo = db.executeQuery({schema: TodoSchema, value: 1});
 
  useEffect(() => {
    db.mergeEntities({
      Todo: {
        1: {
          id: 1,
          text: 'Buy cheese',
          completed: false
        }
      }
    })
  }, [])
 
  return (
    <JSON data={todo} />
  )
}

executeQuery(query)

Executes a query against the database (db.entities).

  • query: required object with shape {schema: normalizr.schema, value: any}

Usage

const TodosComponent = (props) => {
  let db = useDB();
 
  let todos = db.executeQuery({schema: [TodoSchema], value: [1, 2, 3]});
 
  return (
    <JSON data={todos} />
  )
}

getStoredQuery(storedQueryName)

Gets the current query state for the query name provided.

  • storedQueryName: required query name from keys defined in storedQueryDefinitions

Usage

const models = [TodoSchema]
 
let [ DatabaseProvider, useDB ] = createDB(
  models,
  {
    storedQueryDefinitions: {
      ALL_TODOS: {
        schema: [TodoSchema],
        defaultValue: [1, 2, 3]
      }
    }
  }
);
 
const TodosComponent = (props) => {
  let db = useDB();
 
  let allTodosQuery = db.getStoredQuery('ALL_TODOS');
  console.log(allTodosQuery) // -> {schema: [TodoSchema], value: [1, 2, 3]}
  let todos = db.executeQuery(allTodosQuery);
 
  return (
    <JSON data={todos} />
  )
}

executeStoredQuery(storedQueryName)

An alias for db.executeQuery(db.getStoredQuery(storedQueryName)).

  • storedQueryName: required query name from keys defined in storedQueryDefinitions

Usage

const TodosComponent = (props) => {
  let db = useDB();
 
  let todos = db.executeStoredQuery('ALL_TODOS');
 
  return (
    <JSON data={todos} />
  )
}

entities

The root data object from the entity store. Useful to listen to state changes. Could be used to persist parts of state to local storage or to implement undo/redo features. Entities saved to local storage could be used to hydrate the store via createDB's defaultEntities option.

Usage

let [ DatabaseProvider, useDB ] = createDB(
  models,
  {
    defaultEntities: LocalStorageClient.loadState('entities')
  }
);
 
const useEntityListener = () => {
  let db = useDB();
 
  useEffect(() => {
    LocalStorageClient.saveState('entities', db.entities)
  }, [db.entities])
}

storedQueries

The root data object from the query store. Useful to listen to state changes. Could be used to persist parts of state to local storage or to implement undo/redo features.

Usage

const useStoredQueryListener = () => {
  let db = useDB();
 
  useEffect(() => {
    //do something
  }, [db.storedQueries])
}

License

MIT © malerba118

Dependencies (2)

Dev Dependencies (36)

Package Sidebar

Install

npm i react-use-database

Weekly Downloads

1

Version

1.0.1

License

MIT

Unpacked Size

493 kB

Total Files

6

Last publish

Collaborators

  • malerba118