2.0.0 • Published 3 years ago

@brendanatme/redux-model v2.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

Redux-model

Max your redux, scrap your boilerplate

Redux-model is an opinionated layer built on top of Redux designed to cut out your boilerplate, encourage structure and best practices, and give powerful features from line zero. Throw out all those folders and files full of action types, action creators, selectors, effects, and reducers... and create simple models that do what you always want them to by default, while retaining all the flexibility of redux.

Installing

$ npm install --save @brendanatme/redux-model

Usage

// your model code
import { ReduxModel } from "@brendanatme/redux-model";
import * as api from "../my-api";

const productModel = new ReduxModel("products");

// define action creators as thunks or simple action creators
productModel.addAction("FetchById", (id) => async (dispatch) => {
  // leverage pre-existing built-in action creators
  dispatch(productModel.actions.BeginFetch());

  try {
    const item = await api.get(`/products/${id}`);

    // follow data-shape conventions to make your life easier,
    // and your code more consistent and readable
    dispatch(productModel.actions.FetchSuccess({ item }));
  } catch (e) {
    // redux-model gives you the tools that make following best-practices,
    // like proper network status handling,
    // quite trivial
    dispatch(productModel.actions.FetchFailure());
  }
});

export default productModel;
// your redux store startup code
import { configureStore } from "@brendanatme/redux-model";
import productModel from "../my/model/above";

const initStore = configureStore(
  {
    [productModel.key]: productModel.reducer,
  }
  // {}, // (optional) initial state
  // [], // (optional) redux middlewares
);

// call this function to initialize your store
export default initStore;
// your React component code
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import productModel from "../my/model/above";

export default ({ id }) => {
  const dispatch = useDispatch();

  // use built-in selectors to access data consistently
  // without needing to write or repeat procedural code
  const product = useSelector(productModel.selectors.item);
  const network = useSelector(productModel.selectors.network);

  useEffect(() => {
    dispatch(productModel.actions.FetchById(id));
  }, [product]);

  if (network.fetching) {
    return <Loader />;
  }

  if (network.failed) {
    return <Error />;
  }

  return <code>{JSON.stringify(product)}</code>;
};

Docs

Model-based approach

Models contain a key, a reducer, action types, action creators, selectors, and connectors. To instantiate a model, you only need to provide the key:

import { ReduxModel } from "@brendanatme/redux-model";

const productModel = new ReduxModel("products");

You can optionally add your own reducers:

import { ReduxModel } from "@brendanatme/redux-model";

const productModel = new ReduxModel("products", {
  reducers: {
    Foo: (state, action) => ({ ...state, ...action.payload }),
  },
});

You can access action types associated with your created reducers:

import { ReduxModel } from "@brendanatme/redux-model";

const productModel = new ReduxModel("products", {
  reducers: {
    Foo: (state, action) => ({ ...state, ...action.payload }),
  },
});

console.log(productModel.ActionTypes.Foo); // --> 'products/Foo'
console.log(productModel.ActionTypes.Bar); // --> undefined

You can also add your own action creators; types will be created for those as well.

import { ReduxModel } from "@brendanatme/redux-model";

const productModel = new ReduxModel("products", {
  reducers: {
    Foo: (state, action) => ({ ...state, ...action.payload }),
  },
});

productModel.addAction("Bar", async (dispatch) => {
  dispatch({ type: productModel.ActionTypes.Foo, payload: "baz" });
});

console.log(productModel.ActionTypes.Foo); // --> 'products/Foo'
console.log(productModel.ActionTypes.Bar); // --> 'products/Bar'

Next, we'll talk about the out-of-the-box actions redux-model provides, and the default, opinionated way, it handles state.

Opinionated state handling

Redux-model provides a number of actions to make commonplace tasks like fetching data and selecting items easy, consistent, and robust. Take fetching a resource, for example:

import { ReduxModel } from "@brendanatme/redux-model";
import * as api from "../my-api";

const productModel = new ReduxModel("products");

productModel.addAction("FetchById", (id) => async (dispatch) => {
  // initiate the BeginFetch() action
  // this sets our model's network state as such:
  // state.network = { failed: false, fetched: false, fetching: true }
  dispatch(productModel.actions.BeginFetch());

  try {
    const item = await api.get(`/products/${id}`);

    // when we successfully receive a response,
    // we can initiate the FetchSuccess() action
    // to a) update our network state: state.network = { failed: false, fetched: true, fetching: false }
    // and b) update our data state, using the 'item' key for a single resource or the 'items' key for an array
    dispatch(productModel.actions.FetchSuccess({ item }));
  } catch (e) {
    // if our request fails,
    // we can use the FetchFailure() built-in action creator
    // to update our network state to:
    // state.network = { failed: true, fetched: false, fetching: false }
    dispatch(productModel.actions.FetchFailure());
  }
});

// we now have access to the action type as well as the action creator:
console.log(productModel.ActionTypes.FetchById); // --> 'products/FetchById'
console.log(productModel.actions.FetchById); // --> function

// usage:
dispatch(productModel.actions.FetchById("f49wjto"));

Setting Model State

The best way to put data into your model state is by using the FetchSuccess method. This is because redux-model assumes you're loading your data from somewhere else, and so the FetchSuccess action will also automatically update your network status. The FetchSuccess method accepts an object for an argument with a key of item, or items, or both:

// ...

dispatch(productModel.actions.FetchSuccess({

  // for a singular resource
  item: {
    foo: 'bar',
  },

  // for an array of items
  items: [
    { id: 'f5soghl24', name: 'Product A' },
    { id: 'f5soghl25', name: 'Product B' },
  ],

});

Setting your data this way will give you easy access to it elsewhere with redux-model's built-in selectors, and will also give you added built-in functionality such as easily selecting an item from your array:

// get all items
const items = useSelector(productModel.selectors.items);

// ...

// set the selectedItem (using an id from objects within the 'items' array)
dispatch(productModel.actions.SelectItem("f5soghl25"));

// ...

// access the 'selectedItem'
const selectedItem = useSelector(productModel.selectors.selectedItem);

Escape Hatch

If you can't follow the convention, or just plain don't want to, you can use the Update() action creator as an escape hatch to set your state however you want:

// ...

// set a custom property
dispatch(productModel.actions.Update({ foo: "bar" }));

// ...

// get the model's entire state
const { foo } = useSelector(productModel.selectors.state);
2.0.0

3 years ago

1.0.0

3 years ago

0.3.0

4 years ago

0.2.1

4 years ago

0.1.2

4 years ago

0.2.0

4 years ago

0.1.3

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago