Skip to content

Releases: MynockSpit/no-boilerplate-redux

Vee Two: The Resimplification

28 Oct 01:53
Compare
Choose a tag to compare

Well, here we are again. A new version. This time, with more breaking changes! The API has been redone and there are some pretty big core changes.

API changes

The biggest change in V2 is a major shake-up of the API. .select is gone! The calls that used to be store.select().set() are now just store().set(). Now, instead of thinking about which top-level store key you need to edit, just provide a path to the value you're working with.

Additionally, .set() can no longer customize the action. If you need to, you can now use the more explicit .action() method which behaves identically EXCEPT that you either pass it an action ({ payload: value }) or an action creator (() => ({ payload: 'VALUE' })).

.get still exists, but now it takes a path parameter if you want to get deeply (replacing .select().get()), and if you pass it a path, it also takes an optional default value.

initializeStore has been changed to makeStore, and no longer requires you to pass it the reducers object and the combiner separately. makeStore is accessible off the default export from no-boilerplate-redux. To match the changes to how we get the store, we now keep track of multiple stores. To make a new separate store, you must provide a key. (e.g. import { makeStore } from 'no-boilerplate-redux'; makeStore();)

There is a new method called makeReducer which replaces redux's combineReducer. It behaves identically and allows no-boilerplate-redux to have knowledge of the internal structure of your reducers.

The final big thing (the biggest thing) is that the recommended way to access the store is no longer by exporting/importing your created store. This makes your store a singleton and causes some trouble with things like parallel test runs and server-side rendering. Instead, import store from no-boilerplate-redux and then use it as a function to get your store. (e.g. import { store } from 'no-boilerplate-redux'; var myStore = store())

Less functions, more functional

As you may know, if you used the function pattern for setting in v1 (store.select('...').set(() => ... )), the function itself was stored in a hash map, and recalled later -- the key was what was sent in the action. This was done mainly to keep actions serializable, but it never really worked -- the actions were serializable, but without the hash map, you couldn't actually replay them.

Now, in V2, the functions are calculated at the time of the action being firing and the result of the function is what's stored in the action. This has the benefit of being serializable and replayable. However, if you have middleware that delays when actions fire, or changes them to fire asyncronously, the result may be unpredictable. While this has always been the case, the new version is a bit clearer what's happening -- you have the changes in the action itself.

The combineReducers dilemma

I've wanted to remove .select for a long time. From the user's perspective, there's no reason for it to be separate. "It's all one big path, so handle it!" And that's true -- unless you're using redux's combineReducers. combineReducers gives special weight to the top-level keys. Any value below them can be unset, but the top-level keys never can be -- at best they can be set to null. To redux, top-level keys are no more special than any other key -- it's combineReducers that enforces that behavior.

What does this mean for practical purposes? Any number of reducers can act on the same store -- only the restrictions and limitations of combineReducers is preventing you. If your mental model is actions and reducers, this is a good separation of concerns. If your mental model is "let me set anything I please," it's not.

Unfortunately, this means that naively setting values on the store with both a combineReducer reducer and any other reducer produces conflicts -- any time your combineReducer reducer fires, it trims anything it wasn't aware of at start. Enter makeReducer. If you're migrating and need to use vanilla reducers, use it anywhere you would normally use combineReducer. It uses combineReducer and make sure that any prop not previously known to combineReducer gets added to the list.

A note on hooks

For React, the recommendation has always been to use react-redux with no-boilerplate-redux. Version 7 of react-redux introduces the useSelector hook which lets you get parts of state and subscribes your React component to the that state part. If you're using React, you should be using this. I will almost certainly not be including a hook as part of no-boilerplate-redux. NBPR is currently framework agnostic and I intend to keep it that way. Besides, if you combine useSelector with lodash's get, you get a very very similar syntax. My version would be, at best, syntactic sugar with no benefits.

Examples below.

// returns the user id, but doesn't subscribe your component
let userId = store().get('users.nathaniel.id')

// returns the user id and subscribes the component to changes
let userId = useSelector(store => _.get(store, 'users.nathaniel.id'))

.get While the Gettin's Good!

12 Aug 18:02
Compare
Choose a tag to compare

Now with two new methods -- store.select(...).get() and store.get().

  • Use .select(...).get() to get a value from the store at a given path. This is really just sugar on top of a lodash _.get, but it makes it a bit simpler and clearer.
  • By default, select(...).get() returns undefined as the default value -- if you want to specify a different default, pass it to the .get (e.g. select(...).get('MY CUSTOM DEFAULT'))
  • .get isn't fancy -- it's just an alias from store.getState(). Mainly added to round out the API a little. 😃

Also changed in this version:

  • 2a878f1 Simplified the function storing logic. B/c this is only ever used internally, a lot of the if-catching is unnecessary.
  • 809496f Unit tests removed. Integration tests now cover all the same use-cases as the unit tests did.
  • a3345e5 Updated dependencies.
  • 263839b Changed imports from lodash to begin with _ to indicate lodashiness.

Ready, .set, Go!

02 Jun 01:03
Compare
Choose a tag to compare

You can now run .set on your stores directly -- without needing to select first! This has a few behavioural changes from the selected set, but React users should find them understandable.

  • .set can take a function. Like setState, the object returned from that function represents the new store and replaces the entire store with the result. Like setState, the function is passed the current version of the store. Unlike setState, it's a clone of the state, and can safely be modified.
  • .set can also take an object. Like setState, this object is shallowly merged with the current store. (It's not technically a shallow merge, it just behaves like it.)
  • .set on the store has the possibility to fire multiple actions. Any actions customizations passed to a .set directly on a store will be passed to all actions created.

That's it! Have fun!

Annnd... Action!

08 Sep 03:08
Compare
Choose a tag to compare

Action changes!

  • Moved from using a prefix on the action type to an nbpr property on the meta object (designated in the Flux Standard Actions spec) to determine if an action is NBPR or not. This allows for clearer and more customizable action types.

  • Added ability to customize action more deeply. Instead of specifying a string as the second property of .set, you can now pass an object. This object will get merged into the action being sent BEFORE setting type, but AFTER setting the properties needed for NBPR to function (payload.fn, payload.value, payload.path and meta.nbpr).

The Beginning of the End (of Reducers)

26 Jul 01:19
Compare
Choose a tag to compare

Hey, guess what? You don't need reducers and you don't need actions either. Just do this:

store.select('todos').set((todos) => { 
    todos.push({ text: 'remove reducers', completed: true })
    return todos
}

Interested, eh? Check it out!