Vegetarian friendly state for React
##Β Patches
A new website! https://easy-peasy.dev
Finally! The next major version of Easy Peasy has landed. It's been a long time coming, and we are hopeful that the need for a new major release will be a long way off.
This last year has been a challenging one for me on a personal level, so I want to extend my thanks to the patience that the community has shown in me getting this release together. I am aware that the docs still do not reflect the full updated API reflection of v4. I humbly apologise for this and do promise that parity will be achieved. This release already includes a lot of ground work around revamping the docs. I plan to pump a lot more hours into getting the docs into a much more improved and useful structre.
I've become aware of a critical security issue with the version of Immer currently utilised within Easy Peasy <= v4. For that purpose I am issuing this release earlier than I anticipated.
Whilst this is a major release the breaking changes are very minimal. So I sincerely hope that your upgrade will be as fluid as it can be.
memo
helper. You'll need to bring your own memoization library,
for e.g. memoizerific.Upgraded to Immer v8 π
Fixes to support IE11!
You will need to use our new import at the entry of your application to patch Proxy.
import 'easy-peasy/proxy-polyfill';
// The rest of your app code...
In addition to this if you use Map or Set in your state then you will need this additional import at the entry of your application.
import 'easy-peasy/proxy-polyfill';
import 'easy-peasy/map-set-support';
// The rest of your app code...
Improved the bundle size! The Gzip bundle is back down to approximately 10kb.
Fixed issue with computed properties throwing errors when being accessed in certain scopes
Improved the module exports to improve tree shaking capability
Added various bundle type support to the package.json, e.g. "module".
Fixed bugs when rehydrating persisted state
The long wait is over.
v4 has arrived! π
This release includes an insane amount of improvements and fixes, with an ironing over some of the APIs and features that were introduced in v3.
This release will also include a completely overhauled website. This work is still in progress and is likely to continue even after the v4 release. It's a lot of work and I don't want it to be the sole reason for us holding back on the v4 release.
Unfortunately there are breaking changes, however, I expect the breaking changes will only impact a subset of our userbase that are using the more advanced features of Easy Peasy.
createComponentStore
with a useLocalStore
hookThe API for createComponentStore
was a verbose and limited.
Taking learnings from it we have replaced it with a new API; namely useLocalStore
.
function MyCounter() {
const [state, actions] = useLocalStore(() => ({
count: 0,
increment: action((_state) => {
_state.count += 1;
}),
}));
return (
<div>
{state.count}
<button onClick={() => actions.increment()}>+</button>
</div>
);
}
Please see the API documentation for more information.
The persist API received a huge overhaul to the point that it should essentially be considered a rewrite.
We suggest that you read the updated docs and update your implementations accordingly.
Some notable changes include:
immer@7
We used to managed a forked version of Immer as it previously did not support computed properties. This is no longer the case! We now rely on the native Immer package.
A side effect of this is that you may experience browser support issues for browsers that do not support support Proxy.
If you require your application to work on such environments (e.g. Internet Explorer 11 or some versions of React Native on Android) then please ensure that you import and execute the enableES5
feature from Immer.
import { enableES5 } from "immer";
enableES5();
redux-thunk
binding to grant user defined middleware higher priorityThis will allow you to influence thunks prior to their execution. For advanced cases.
This likely will have zero impact on anyone, but given the nature of the change we are marking it as breaking.
Easy Peasy will no longer catch any errors within your thunks and dispatch a "failed" state action. If you wish to explicitly mark your thunk as failed, so that an action listener can respond to it accordingly then you need to use the new fail
helper that is provided to thunks.
const model = {
myNaughtyThunk: thunk((actions, payload, helpers) => {
try {
await axios.get('/an-endpoint-that-fails');
} catch (err) {
// This will dispatch a @thunk.myNaughtyThunk(fail) action with the err attached
fail(err);
}
})
};
Error handling is now explicitly your responsibility.
The actions that are dispatch to represent thunk states have been updated. Taking the example above here are the possible action types that will be dispatched and visible in the redux dev tools:
@thunk.myThunk(start)
Dispatched at the start of a thunk execution.
@thunk.myThunk(success)
Dispatched when a thunk has completed - i.e. with no uncaught errors occurring.
@thunk.myThunk(fail)
Dispatched if the fail
helper was called. In this case the @thunk.myThunk(success)
would not have been dispatched.
Listeners (actionOn and thunkOn) will now by default only respond to the "success" event of a thunk. If you wish to handle the "fail" events then you will need to explicitly resolve them.
onAddTodoFailed: actionOn(
(actions) => actions.saveTodo.failType,
(state, target) => {
state.auditLog.push(`Failed to save todo: ${target.payload}`);
}
);
unstable_effectOn
*Note: this is an experimental API. We are pre-releasing it to allow for early feedback. The API is subject to breaking changes with any release of Easy Peasy. As such we have prefixed the API with "unstable*", much like React does with its experimental APIs. Once the API has stabilised the "unstable*" prefix will be removed and semver based releases will be respected.*
Allows you to declare an effect within your model which will execute every time the targeted state changes.
Two arguments are provided to unstable_effectOn; namely the stateResolvers
and the handler
. The stateResolvers
are an array of functions which should resolve the target state that should be tracked. When the tracked state changes the handler
function is executed.
The handler
can be asynchronous or synchronous. It receives the store actions, a change
object containing the prev
values for the targeted state and the current
values as well as the action
that caused the change in state. It additionally receives a helper
argument allowing you to access the store state etc.
The handler
additionally allows you to return a dispose function, which will be executed prior to the next execution of the handler
. This can be useful in performing things like API call cancellation etc.
import { unstable_effectOn } from 'easy-peasy';
const todosModel = {
items: [],
saving: false,
setSaving: action((state, payload) => {
state.saving = payload;
}),
unstable_effectOn(
// Provide an array of "stateResolvers" to resolve the targeted state:
[state => state.items],
// Provide a handler which will execute every time the targeted state changes:
async (actions, change) => {
const [items] = change.current;
actions.setSaving(true);
await todosService.save(items);
actions.setSaving(false);
}
)
};
Simply pass your injections as a prop to the Provider
for the context store.
See the updated docs for more information.
Previously if you executed a computed property it would always resolve against the latest version of the store state. Now it will operate against the state at the moment of time it was pulled out from.
In doing this we also addressed a strange case where you may get an error for invalid computed property access.
This will address any issues where you may have been providing model definitions to different stores.
Phew. Too many to mention. Just take our word for it. Tons of nitty issues have been addressed.
Previously if you defined a model containing generic state, like below, TypeScript would break within your actions.
interface StoreModel<K> {
data: K;
updateData: Action<StoreModel<K>, K>;
}
const numberStoreModel: StoreModel<number> = {
data: 1337,
updateData: action((state, payload) => {
// A TypeScript would be thrown at this point
// π
state.data = payload;
}),
};
Unfortunately we were unable to directly resolve the case of generic properties due to current limitations with the TypeScript type system. We created a StackOverflow question which details the problem.
In a gist; the issue is that Easy Peasy's underlying State
and Action
types map over the user provider model types in order to filter down to types that represent state and actions respectively. However, when defining a generic state, TypeScript assumes that the generic state intersects with types that are trying to be filtered out of each case. Therefore the filtering ends up always removing your generic state.
To resolve the case of generic state we have introduce a new API helper. Any time you wish to have a generic state value within your model, simply wrap it with the Generic
type, and then assigned the associated value within the model instance using the generic
helper.
import { Generic, generic } from "easy-peasy";
interface StoreModel<K> {
data: Generic<K>; // π
updateData: Action<StoreModel<K>, K>;
}
const numberStoreModel: StoreModel<number> = {
data: generic(1337), // π
updateData: action((state, payload) => {
// Note that you don't need to wrap the payload with the
// helper π
state.data = payload;
}),
};
numberStoreModel.getState().data;
// 1337
typeof numberStoreModel.getState().data;
// number
Note how you only need to use the helper at the point of defining the initial value of the generic model state. Within your actions and anywhere you consume the state you would treat the value as the underlying generic value (i.e. a number
in the example).
Previously, if you had a model more than 6 levels deep, in terms of object structure, the TypeScript mapping wouldn't work as expected. This is no longer the case.
If you assigned a class instance to your state the typings from getState
/ useStoreState
etc would report your type as being something similar to:
With this fix your state will correctly be reported back as the actual class type (Person
in the examples case).
The VSCode intellisense for actions were showing state. The types have been patched so that only actions will be displayed.
There were cases if you had a nested action and only primitive state next to it that you would see the nested action. This is no longer the case.
If you used a state resolver array within your computed properties, whilst using TypeScript, the inferred types based on the resolved state was incorrect. This is now fixed.
This is a no-op release that just fixes latest on npm
due to an accidental publish.
reducer
comment block in the Interop recipe.: #310Huge thanks to @joelmoss, @loveforweb, @gamtiq, @jaredmeakin, and @nickmeldrum for helping!
Huge thanks to @crissdev for helping!