redux-storm

Strongly Talented ORM

Usage no npm install needed!

<script type="module">
  import reduxStorm from 'https://cdn.skypack.dev/redux-storm';
</script>

README

logo

Strongly Talented ORM

Prerequisites

React-redux: this is a plugin for React-Redux, therefore you must have it installed before you can start.

Installation

npm install redux-storm --save

Usage

Storm supports two types of data: collections and sets. The first is used to handle array-like data, the latter handles object-like data.

Collections

Let's say you need to store a list of users coming from an API. You need to create a model for that;

class Users {
    static $type = 'collection';
    static $table = 'users';
    static schema() {
        return {
            id: null,
            name: ''
        }
    }
}

The syntax above is allowed only if you have the @babel/plugin-proposal-class-properties plugin installed. If you don't want to use the babel plugin you can turn that declaration into something like the snippet you'll find below. However, this documentation will tend to use the syntax from the first example

const Users = {
    $type: 'collection',
    $table: 'users',
    schema() {
        return {
            id: null,
            name: ''
        }
    }
}

What have we done with that? We defined a table name ($table) for this model (that will be used to create the reducer in redux) and a type ($type), telling to storm to treat this model as a collection of users; we then defined a schema function that returns the shape of the user, within the default values (can be null).

Then we need to tell redux to use our model, so we add this to the reducers registration:

import { combineReducers } from 'redux';
import Users from '../models/Users';
// use this function to register your models
import { register } from 'redux-storm';

const reducers = combineReducers({
   // other reducers you might be using
   // ......
   //
   
   // also, this method accepts multiple models, separated by a comma, like an array
  ...register(Users),
});

export default reducers;

That's it! We are ready to go and implement the logic!

Let's have a look to a possible implementation

import React, { useEffect } from 'react';
import { useModel } from 'redux-storm';
import UserModel from '../models/Users';

function UserList() {
    const Users = useModel(UserModel);
    const listOfUsers = Users.all();

    useEffect(() => {
        fetch('/api/users').then(r => r.json()).then(data => {
            Users.insert(data);
        });
    }, []);
    
    return listOfUsers.map(user => <div key={user.id}>{ user.name });
}

We want to make sure though that duplicates are not added, therefore we need a primary key

class Users {
    static $type = 'collection';
    static $table = 'users';
    static $primary = 'id';
    
    static schema() {
        return {
            id: null,
            name: ''
        }
    }
}

Now if we add another user within the same ID (this however shouldn't be a thing!), it won't be added because.

When you are using a collection, you can also define a state ($state) for you model

class Users {
    static $type = 'collection';
    static $table = 'users';
    
    static $state() {
        return {
            fetching: true
        }
    }
    static schema() {
        return {
            id: null,
            name: ''
        }
    }
}

We can now use the state inside our component, to show, for example, a loader while fetching the data

import React, { useEffect } from 'react';
import { useModel } from 'redux-storm';
import UserModel from '../models/Users';

function UserList() {
    const Users = useModel(UserModel);
    const listOfUsers = Users.all();

    useEffect(() => {
        fetch('/api/users').then(r => r.json()).then(data => {
            Users.insert(data);
        });
    }, []);
    
    if (User.state().fetching) { return ( <div>Loading</div> )};
    return listOfUsers.map(user => <div key={user.id}>{ user.name });
}

But how do we tell now the fetching is completed? We'll use the .commit method:

import React, { useEffect } from 'react';
import { useModel } from 'redux-storm';
import UserModel from '../models/Users';

function UserList() {
    const Users = useModel(UserModel);
    const listOfUsers = Users.all();

    useEffect(() => {
        fetch('/api/users').then(r => r.json()).then(data => {
            Users.insert(data);
            Users.commit(state => {
                state.fetching = false;
                return state;
            })
        });
    }, []);
    
    if (User.state().fetching) { 
        return ( <div>Loading</div> )
    };
    return listOfUsers.map(user => <div key={user.id}>{ user.name });
}

The component will now react accordingly and display the list when the users table is populated.

useModel for collections

When used on a collection, the useModel exposes the following methods:

all(): Array

Returns all the records for the specified model

find(predicateOrPrimaryKey: Function | Any) : Array| Object

Returns an array of records given the specified condition. Example:

const Users = useModel(UserModel);
const listOfUsers = Users.find(1); // returns the user matching the primary key
const listOfUsers2 = Users.find((user) => user.name === 'me'); // returns all the user with the name 'me'
exists(): Boolean

Returns whether the table exists. Likely to be false before any data gets added to the store.

any(): Boolean

Returns whether the table contains any data.

state(): Object

Returns the state defined in the model.

commit(changes: Function)

Allows to mutate the state of the model.

insert(data: Array | Object)

Adds a record or multiple records preventing any duplication (using the primary key or the ID to distinct)

update(data: Object, predicateOrPrimaryKey: Function | Any)

Updated a record or multiple records according to the predicate Example:

const Users = useModel(UserModel);

// updates the name to 'another me' to the user with ID 1
Users.update({
    name: 'another me'
}, 1);

// updates the name to 'another me' to every user whose age is greater that 10
Users.update({
    name: 'another me'
}, (user) => user.age > 10);
delete(predicateOrPrimaryKey: Function | Any)

Deleted the record or multiple records according to the predicate

const Users = useModel(UserModel);

// deletes the user with ID 1
Users.delete(1);

// deletes to every user whose name is nome me
Users.delete((user) => user.name !== 'me');
insertOrUpdate(data: Array | Object, predicateOrPrimaryKey: Function | Any)

Adds a new record or mulitple records if the ones matching the predicate or the primary key are not found

Example:

const Users = useModel(UserModel);

// adds the user to the store
Users.insert({
    id: 1
    name: 'me'
});

// it only adds 'another me' but updates 'me' with 'me again' because they have the same ID
Users.insertOrUpdate([
    {
        id: 2,
        name: 'another me'
    },
    {
        id: 1,
        name: 'me again'
    }
])

Sets

Let's say we need to store whether the user is authenticated or not in our application. For this purpose we are going to create a different type of model:

class Authentication {
    static $type = 'set';
    static $table = 'auth';

    static schema() {
        return {
            isLoggedIn: false,
            username: null
        }
    }
}

then we add it to store like we did for the Users model:

import { combineReducers } from 'redux';
import Users from '../models/Users';
import Authentication from '../models/Authentication';
// use this function to register your models
import { register } from 'redux-storm';

const reducers = combineReducers({
   // other reducers you might be using
   // ......
   //
   
  ...register(
      Users, 
      Authentication
  ),
});

export default reducers;

Let's return to our component:

import React, { useEffect } from 'react';
import { useModel } from 'redux-storm';
import UserModel from '../models/Users';
import AuthenticationModel from '../models/Authentication';


function UserList() {
    const Users = useModel(UserModel);
    const Auth = useModel(AuthenticationModel);
    const loggedUser = Auth.get();

    const listOfUsers = Users.all();

    useEffect(() => {
        Auth.set('username', 'me1245');
        Auth.set('isLoggedIn', true);

        fetch('/api/users').then(r => r.json()).then(data => {
            Users.insert(data);
        });
    }, []);
    
    if (User.state().fetching) { return ( <div>Loading</div> )};

    return (
        <div>
            {
                loggedUser && <h1>Welcome back { loggedUser.username }</h1>
            }
            <div className="list">
                { listOfUsers.map(user => <div key={user.id}>{ user.name }); }
            </div>
        </div>
    )
}

You might have noticed the set type does not provide a state: this is because a set is a state itself, so it would be redundant.

useModel for set

When used on a set, the useModel exposes the following methods:

set(key: String, data: Object | Any)

Sets an object against the model

unset(key: String)

Removes the specified key from the set

Example

Auth.set('name', 'me');
Auth.set('age', 30);
Auth.set('birthday', {
    day: 30,
    month: 12
})

// this will remove the age
Auth.unset('age');

// this will only remove the month 
Auth.unset('birthday.month')
get(key: String | null)

Returns the entire object if no params are passed or the specified key

Example

Auth.set('name', 'me');
Auth.set('age', 30);
Auth.set('birthday', {
    day: 30,
    month: 12
});

Auth.get() // return the entire object
Auth.get('age') // only returns the age
Auth.get('birthday.day') // only returns 30

Non hooks version

Although it is recommended to use the hooks based version, you can still use the old redux API with connect.

import React, { useEffect } from 'react';
import { withModels } from 'redux-storm';
import UserModel from '../models/Users';
import AuthenticationModel from '../models/Authentication';


function UserList({ Users, Authentication }) {
    const loggedUser = Auth.get();

    const listOfUsers = Users.all();

    useEffect(() => {
        Auth
            .set('username', 'me1245')
            .set('isLoggedIn', true)

        fetch('/api/users').then(r => r.json()).then(data => {
            Users.insert(data);
        });
    }, []);
    
    if (User.state().fetching) { return ( <div>Loading</div> )};

    return (
        <div>
            {
                loggedUser && <h1>Welcome back { loggedUser.username }</h1>
            }
            <div className="list">
                { listOfUsers.map(user => <div key={user.id}>{ user.name }); }
            </div>
        </div>
    )
}


export default connect()(withModels(UserList, UsersModel, AuthenticationModel));
withModels(Component, ...models)

Accepts the component and a list of models to use that will be passed as props. Always use this inside the connect and not the other way

Yes:

export default connect()(withModels(UserList, UsersModel, AuthenticationModel));

No:

export default withModels(connect()(UserList), UsersModel, AuthenticationModel));