Skip to content

Vee Two: The Resimplification

Latest
Compare
Choose a tag to compare
@MynockSpit MynockSpit released this 28 Oct 01:53
· 12 commits to master since this release

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'))