1.1.3 • Published 2 years ago

redux-general-store v1.1.3

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

Welcome to the Redux General Store!

A data-agnostic Redux store alternative that dramatically reduces boilerplate

We all love redux, sure, but everyone knows it has one tiny flaw, which is the amount of boilerplate necessary to get the redux store up and running. Well no more!

The Redux General Store generalizes thunks and reducers so that one set of thunks can serve any model, and reducers are generated for each model on-the-fly. The trade-off is more opinionation: you must specify which model and route to use when calling the thunk.

For example:

getUsers()

becomes:

genericGet('/api/', 'users')

In this example , both strings passed in should be substituted with constants to guard against typo bugs. That turns it into:

genericGet(api, users)

See? It is slightly more 'wordy' than the original line, but in exchange for typing few more characters, you get a redux store that is about 10 lines of code and doesn't get larger with more models. Adding a new slice of data to your store is as easy as adding the name of the model to the 'models' array (or using the generateReducer function provided) and that's it.

The General Store does not prevent you from adding thunks or reducers in the traditional way. You can even use the CRUD suite from the General Store and make a new reducer for that one thunk the General Store doesn't provide!

A small React application using The General General Store (with a LOT of comments explaining its use) can be found here: https://github.com/LLLisa/RGSTest <--check out that redux store!

!important: This is not intended as a complete replacement for all redux functionality. For instance, the thunks provided by the general store can only GET all instances of a model. It does not GET all instances WHERE a condition is met (well, it can, but you probably shouldn't bother). Also, no consideration is given to security here. You are expected to take care of these sorts of situations in your REST API routes and/or by building your own thunks and reducers to cover them.

The Redux General Store is intended for general, non-secure CRUD operations between your frontend and your API. It does not prevent you from adding thunks or reducers in the traditional way. You can even use the CRUD suite from the General Store and make a new reducer for that one thunk the General Store doesn't provide!

So let's get started!

Getting Started

First, do the old

$ npm install redux-general-store --save-dev

Then, in your redux store/index file:

import GeneralStore from 'redux-general-store'

What you have just imported is the GeneralStore class. Create an instance of it and pass in the base URL of your API as the first argument, and an array containing the names of your data models as the second argument. It will look something like this:

export const GS = new GeneralStore('http://localhost:42069', [ 'users', 'accounts', 'files', ]);

You can put the export statement elsewhere if you wish. When the GS object is created, it will automatically construct reducers for each of the models passed in the second argument.

The GS Object

Properties

The GS object is returned when you create an instance of the GeneralStore class using the new keyword. It has three properties:

  • GS.baseUrl

(String) This is the base url to your api. Something like http://localhost:42069 or www.fsf.org or something. The rest of the api route (pun intended) will be appended to this url.

  • GS.models

(Array) This is the array of models you passed in when the GS object was created. This is used to create reducers for each model and is useful wherever you need a list of all of the models handled by The Redux General Store.

  • GS.reducerBody

(Object) This is where things start getting good. This object contains all of the reducers generated when the GS object is created. This is passed directly into the redux combineReducers function like so:

const reducer = combineReducers({ GS.reducerBody})

If adding more reducers besides the ones generated by the GS object, use the spread operator like so:

const reducer = combineReducers({
  /**place non-general reducers here */ ...GS.reducerBody,
});

CRUD Methods

There are 4 CRUD methods on the GS object:

  • GS.genericGet(route, model)

This method is will attempt to use an HTTP GET route on a model (or table or slice), returning all rows from the specified table. It expects to receive an array as a response. Api route urls typically look something like this:

http://localhost:42069/api/users

This is what is constructed when genericGet is called: The baseUrl is the one passed in when the GS object is created, the api route (/api) and the model (users) are passed in when calling the method. The route is then constructed like this:

${this.baseUrl}${route}/${model}

The model argument is kept separate from the rest of the url here because it is also used to dispatch the result of the api call to the reducer for that model. Neat!

  • GS.genericPost(route, model, data)

This method will attempt to add a row (or instance) to a database table using an HTTP POST route and expects to receive the created item as a response. The route and model arguments are the same as in the previous method, and the data object here must be an object. It should look something like this:

  {
    firstName: 'Homer',
    lastName: 'Simpson',
    email: 'chunkylover53@aol.com'
  }

The data is intended to be inserted into the proper table with the column names matching the keys on the data object. Server-side and database validation errors are not handled by The Redux General Store and should be processed separately.

  • GS.genericPut(route, model, data, identifier(optional))

This method will attempt to send an HTTP PUT request to the server with the intention of updating a particular row in a database and expects to receive the updated item as a response. The route and model arguments are the same as in the previous methods, but if the optional identifier argument is provided, the data object only needs to contain the data to update on the intended row. The identifier argument is an object used to identify which row in the database to update. For example:

GS.genericPut(api, users, { jobTitle: Snow Plow Driver }, { firstName:Homer })

The api route handler should be configured find the proper row by using the identifier object as a search parameter and update the information accordingly, like this:

/*try-catch omitted for brevity*/

app.put('/api/users', async (req, res, next) => {
  const userToUpdate = await User.findAll({
    where: {
      firstName: req.body.identifier
    }
  })
  const response = await userToUpdate.update(req.body.data)
  res.send(response)
}

However, an easier way to do this is to simply pass the entire updated data object like so:

GS.genericPut(api, users, updatedUser)

or

GS.genericPut(api, users, { ...selectedUser, ...updatedInfo })

If an identifier argument is not provided, the genericPut method will look on the data object for a property called 'id' and use that as an identifier. The api route handler can use this to find the proper row using the id property as the primary key. The req.body generated by this method will always be an object in the shape of req.body: {data, identifier}

  • GS.genericDelete(route, model, identifier)

This one is just like it says on the tin. It will attempt to send an HTTP DELETE request to the provided url and expects to receive the deleted item as a response. The identifier argument (usually the primary key) should be an object in this form:

{ id:4 }

or

{ lastName: 'Grimes' }

If something other than the primary key is used as an identifier, make sure to reflect that in your api route handler.

That's it for the CRUD methods!

Additional Methods

There are two utility methods on the GS object. It is probably best to ignore them both, but there may be use cases for them, so here ya go:

  • GS.generateReducer(model)

This method returns a reducer function with Get, Post, Put, and Delete functionality for the specified model. It looks like this:

generateReducer = (model) => {
    return (state = [], action) => {
      if (action.type === `GET_${model}`) return action.payload;
      if (action.type === `POST_${model}`) return [...state, action.payload];
      if (action.type === `PUT_${model}`) return state.map((x) => x.id === action.payload.id ? action.payload : x);
      if (action.type === `DELETE_${model}`) return state.filter((x) => x.id !== action.payload.id);
      return state;
    };
  };

Note that the action types here don't quite follow the typical redux naming convention; instead of GET_USERS we have GET_users. This allows us to use the same variable to build the api url as we use to dispatch state changes. Pretty cool, huh?

Also note that every generic reducer returns an empty array by default.

This method will be useful if you want to generate a one-off generic reducer for a particular model. Just:

const stoneCutterReducer = GS.generateReducer(stoneCutters)

Then add it to the combineReducers function provided by redux.

const reducer = combineReducers({ stoneCutterReducer, ...GS.reducerBody })

This is also where you would pass in a custom reducer built for a thunk not provided by the General Store. Say you want data in the form of a string or object in your redux store (remember, the General Store holds everything as arrays). Build your own reducer for that model and pass it into combineReducers like in the example above.

  • GS.generateReducerBody(models)

This method is used when the GS object is created. It calls generateReducer on each model that was passed in when the GS object was created and returns an object which is a collection of those reducers. It is typically passed into the redux combineReducers function like so:

const reducer = combineReducers({ GS.reducerBody})

That's the entire GS object, three properties and six methods. That's all it takes to build an entire redux store regardless of how many data models are required. However, the results you get using the General Store will largely depend on how your RESTful API is set up. To that end, here are some...

Typical API Routes

Here is a typical GET route that returns all rows of a given table:

app.get('/api/:model', async (req, res, next) => {
  try {
    let tableName = req.params.model;
    const regExp = /[A-Z]/;
    if (regExp.test(tableName)) tableName = `"${tableName}"`;
    const response = await db.query(`SELECT * FROM ${tableName} ;`);
    res.send(response[0]);
  } catch (error) {
    next(error);
  }
});

This route is used to retrieve data from any table whose name is passed in through the url, accessed via req.params.model. We are using a postgresql database here, which requires us to either only have table names with all lowercase letters or account for this by surrounding those table names with quotes. We have also opted for a raw SQL query instead of using the sequelize Model.findall() method because transmuting the model names passed in into their singular, uppercased forms is harder than just using a regex test and a raw db.query(). This method follows the generic style of The Redux General Store, but it's veering sharply away from the RESTful API paradigm, which is not necessary to do at all.

You can (and probably should) write traditional RESTful API routes like this:

app.put('/api/users', async (req, res, next) => {
  try {
    const userToUpdate = await User.findByPk(req.body.identifier.id);
    const data = req.body.data;
    const response = await userToUpdate.update(data);
    res.send(response);
  } catch (error) {
    next(error);
  }
});

The Redux General Store passes the identifier (here, the Primary Key) and the updated data as separate objects in a PUT route. They are accessed via req.body.data and req.body.identifier.

And just to round out our CRUD suite:

app.post('/api/users', async (req, res, next) => {
  try {
    const data = req.body;
    const response = await User.create(data);
    res.send(response);
  } catch (error) {
    next(error);
  }
});
app.delete('/api/users', async (req, res, next) => {
  try {
    const doomedUser = await User.findByPk(req.body.id);
    await doomedUser.destroy();
    res.send(doomedUser);
  } catch (error) {
    next(error);
  }
});

React Implementation

In a react component, you won't be able to import your thunks individually because they are all on the GS object. So just import the GS object and access them from there!

import store, { GS } from '../store';

Access them with GS.genericGet() or GS.genericPut() etc.

If using class components, your mapDispatchToProps method will look only slightly different because of the extra arguments required:

const mapDispatchToProps = (dispatch) => {
  return {
    genericGet: (route, model) => dispatch(GS.genericGet(route, model)),
    genericPost: (route, model, data) => dispatch(GS.genericPost(route, model, data)),
    genericPut: (route, model, data, identifier) => dispatch(GS.genericPut(route, model, data, identifier)),
    genericDelete: (route, model, identifier) => dispatch(GS.genericDelete(route, model, identifier)),
  };
};

With React Hooks, several exciting new possibilities open up! For example, we eliminate the need for a mapStateToProps function. Instead, we use

const dispatch = useDispatch();

and then call our GS methods with it:

dispatch(GS.genericGet(apiRoute, model))

Note that when updating the redux store, we must use the dispatch method from the useDispatch hook rather than one returned by the useReducer hook. This is because useReducer only updates local state, not state in the Redux store. You can still use the GS.generateReducer with useReducer though! Just remember that the type names are generated from the string passed into the GS.generateReducer function:

const [names, dispatch] = useReducer(GS.generateReducer('names'), [
    'Bart',
    'Lisa',
  ]);

dispatch({ type: 'POST_names', payload: 'Maggie' });

Note the action type: POST_names. As long as the naming convention is followed, it will work just fine!

At this point you are ready to use these methods in your react component. Stay tuned for demonstrations of other implementations!

-LK

1.1.3

2 years ago

1.1.2

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.0

2 years ago