Closed gsklee closed 4 years ago
PS. The adapter pattern is good, but I think it should remain invisible for officially supported storages (ie. localStorage, AsyncStorage, etc.). End users should only be required to construct a storage using adapter
only when trying to fit an unsupported storage into the lib.
Hi @gsklee, thanks for opening an issue! As I’ll elaborate on below I’m faced with a bit of a dilemma with the direction the v1.0 API is heading which touches some of your remarks, so I appreciate your feedback.
I’d like to share the consideration I made in relation to the points you’ve made, not to defend them, but to ensure we’re on the same page.
INIT
action. (More info)index.js
. You could argue this highlights the problem of including the adapters as part of this package.. this touches your third remark.filter
storage enhancer provides was part of the api/package for the majority of the releases for much the same reason you mention; it’s a common thing to do/require. There were two reasons that motivated me to move it to a separate repo despite that.
I find myself agreeing with your remark that it seems strange that you require an adapter for localStorage given the name of this package. In that sense I like the api you propose in your last code snippet. What’s important to realise though is that you would be required to include a storage enhancer to serialise/deserialise (in other words JSON.stringify/parse) your store state. (This is exactly the same consequence that the proposed solution for #16 has, but in this case you get something back in return, which is that you don’t require an adapter anymore). It could look something like this:
import {compose, createStore} from 'redux';
import {persistState, localStorage} from 'redux-localstorage';
import filter from 'redux-localstorage-filter';
const storage = compose(
serializer(JSON.stringify, JSON.parse),
filter('nested.key')
)(localStorage)
const store = compose(
persistState(storage)
)(createStore)
The above snippet illustrates something else I’ve been thinking about, which is that quite a few use cases for storage enhancers simple want to transform the state in some way, creating a storage enhancer for each seems a little verbose. An interesting alternative might be to simply create a transform
storage enhancer that applies an Array of transform functions. I’d have to look into it further, but this could potentially also make it easier to use e.g. lodash-fp’s version of _.pick to replace redux-localstorage-filter
.
Apologies for the amount of info to digest, might have to split this discussion up into a couple of issues, but we'll see how it goes. Like I said I'd appreciate your input/feedback. cc @slybridges, @vicentedealencar, @ruprict, @eisisig, anyone else reading this.
A more generic transform
storage enhancer sounds like a good idea. I'd also like to point out that
compose(
serializer(JSON.stringify, JSON.parse),
filter('nested.key')
)(localStorage)
is simply just
serializer(JSON.stringify, JSON.parse)(
filter('nested.key')(localStorage)
)
So if we're willing to sacrifice a little bit of performance we can reverse the order of application
filter('nested.key')(
serializer(JSON.stringify, JSON.parse)(localStorage)
)
And simply do export const localStorage = serializer(JSON.stringify, JSON.parse)(window.localStorage)
in the package, simplifying user code to
compose(
filter('nested.key')
)(localStorage)
I like the transform
idea too.
I also like what @gsklee demonstrated, having default exports
so that the most basic use doesn't need setting any enhancer
/serializer
/transformer
.
Might not have understood correctly, but by removing the need for an adapter (for storage backends with an API similar to localStorage), why would this package still include an export for said storage backends?
I actually fully agree with the sentiment that it would be great to apply the JSON stringification step by default (that's why it's currently part of the adapter), but doing so gives rise to the issue described in #16.
It's possible to apply the JSON stringification step as part of the defaultStorage
that's assigned internally if you don't provide a storage argument to persistState
(thus making the most basic use easier). If you provide your own storage instance however, (because you want to e.g. persist only a subset of store state), then I don't see any other way then that developers have to ensure that the state is stringified themselves.
That said, if the transformState
storage enhancer discussed above becomes part of the package (further research pending), then it's only a matter of adding JSON.stringify
and JSON.parse
as transforms, which might not be so bad.
Thoughts?
I've done some work on the generic transformState
enhancer to feel it out. It's signature is currently as follows tranformState(down, up)
, whereby the arguments down
and up
both accept an array of transformation functions. This enables the following:
import _ from 'lodash-fp'
import {compose, createStore} from 'redux';
import persistState, {transformState} from 'redux-localstorage';
const storage = compose(
transformState([
_.pick('todos'),
JSON.stringify,
btoa
], [
atob,
JSON.parse
])
)(localStorage)
This looks pretty good to me. It solves the current issue with encryption. It also makes it easy to use lodash-fp functions such as _.pick
and _.omit
which are more generic and versatile than the number of custom storage enhancers that have been created to solve the same need. It also reduces the pain of having to JSON.stringify/parse the state yourself.
EDIT: _.pick
and _.omit
unfortunately don't support nested.keys
... there doesn't appear to be a method to get the subset of an object directly. Would still be possible through a combination of _.get
and _.set
for example, but that's not really great; requires too much effort.
@elgerlambert Looks cool, if I'll suggest something it'd be that the signature of transformState
can be transformState({up, down})
so by looking at the code alone I can tell right away which part defines the "up" transformation and vice versa, without looking at the doc.
@gsklee like this?
const storage = compose(
transformState({
down: [
_.pick('todos'),
JSON.stringify,
btoa
],
up: [
atob,
JSON.parse
]
})
)(localStorage);
Yep.
In my eagerness to simplify, I overlooked something important in my reasoning.
In order to support asynchronous storage backends (such as React Native’s AsyncStorage), the way I see it, localStorage will have to be wrapped by some sort of an adapter; either to call a callback or to return a promise (or an observable) instead..
Hey, I was wondering if there was any progress on improving the new API?
Hi @tjallingt,
The recently released rc5
has taken a lot of inspiration from what has been discussed in this issue. It includes the transformState enhancer proposed above.
Furthermore, the default adapter export still includes JSON (de)serialisation, but now it's also possible to access the adapter without (de)serialisation applied to it which provides a solution for the issue that was raised in #16 with regards to encryption.
There's one suggestion that hasn't been incorporated into rc5
and that's @gsklee's suggestion to expose adapter(window.localStorage)
directly as localStorage
. The argument here is to maintain consistency & an attempt to enforce the idea that you can pass in any persistence layer you want.
Did you have any feedback or things that were discussed above that you would "+1" or were you more curious when 1.0.0
would (finally) be published as latest on npm?
Hey @elgerlambert thanks for the reply,
I was mostly interested in when the release of the changes as described above would be.
As for my two cents on exporting localStorage
:
I understand the need for consistency and agree with the argument that adapter(window.localStorage)
is clearer overall.
I'd suggest shipping this package with only the localStorage
adapter and putting the others in their own redux-localstorage-adapter-[storage-backend]
packages.
This would make the "default" usecase look like:
import persistState, { mergePersistedState, adapter } from 'redux-localstorage'
On another note: although i understand your reasoning for keeping filter
in its own package i dislike that decision because it makes this package much more difficult to use.
For example I have a lot of trouble telling what the following section does:
const storage = compose(
filter('playlist')
)(adapter(window.sessionStorage));
A few thoughts:
I don't see any reason to use this library with only
persistState
but notmergePersistedState
, so it doesn't make sense to makepersistState
as _the_ default export. Please consider making it work like this instead:To this bloated mess:
The weirdest part is - I don't understand why we have to create the
localStorage
storage ourselves. Shouldn't this be handled by the library which is called preciselyredux-localstorage
?If there is no justified benefit, couldn't we make it a bit simpler like this?
Where the
localStorage
export is simplyadapter(window.localStorage)
.