
Research
Malicious npm Packages Impersonate Flashbots SDKs, Targeting Ethereum Wallet Credentials
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
Infinitely composable state + actions
Write data models that would have worked 5 years ago and will still work in 5 years time.
A data store that combines state + actions into a single model object, composed of other model objects.
By returning the state changes (or a promise with the changes) in your model functions, you can assemble powerful applications with no boilerplate, asynchronous programming, lazy loading, and type safety, all the while only importing the library in a single file.
import { createStore } from 'derpy';
const store = createStore({
name: 'World',
setNameTo(aNewName) {
return { name: aNewName };
}
});
store.subscribe((model) => document.body.innerHTML = `Hello ${model.name}`);
store.update();
Calling store.update()
initially renders "Hello World"
.
Calling store.model.setNameTo('😋')
anytime renders "Hello 😋"
and so on.
import { createStore } from 'derpy';
const store = createStore({
count: 0,
down() {
return { count: this.count - 1 };
},
up() {
return { count: this.count + 1 };
}
});
store.subscribe((model) => document.body.innerHTML = `Count: ${model.count}`);
store.update();
Calling store.model.down()
or store.model.up()
updates the count and calls the function passed to store.subscribe
with the new data.
For example, adding this code will increment the counter and render it every second:
setInterval(store.model.up, 1000);
createStore
Creates a store from a source object, deep copying all values and proxying all functions to call store.update
when executed.
Can optionally receive a second argument to customize behavior:
const store = createStore(model, {
merge(target, source, createProxyFunction) {
// Customize the way `source` is merged into `target`.
// Don't forget to call `createProxyFunction` on functions to make them update the state automatically!
},
callFunction(fn, state, args) {
// Customize the way functions are called.
// If you prefer not to use `this`, you can change the
// signature of your functions to `(state, ...args) => changes`
// or even `(state) => (...args) => changes`
}
});
This function performs a shallow merge instead of the default deep merge:
const store = createStore(model, {
merge(target, source, createProxyFunction) {
for (let key in source) {
if (typeof source[key] === 'function') {
// Proxy functions so they automatically resolve promises and update state
target[key] = createProxyFunction(source[key], target);
}
else {
target[key] = source[key]; // Yay, shallow merge! 🎉
}
}
return target;
}
});
If you prefer using another format for your functions like (state, ...args):
const store = createStore({
count: 0,
down: (state) => ({ count: state.count - 1 }),
up: (state) => ({ count: state.count + 1 }),
add: (state, value) => ({ count: state.count + value })
}, {
callFunction: (fn, state, args) => fn(state, ...args)
});
Or if you like (state) => (...args) more:
const store = createStore({
count: 0,
down: (state) => () => ({ count: state.count - 1 }),
up: (state) => () => ({ count: state.count + 1 }),
add: (state) => (value) => ({ count: state.count + value })
}, {
callFunction: (fn, state, args) => fn(state)(...args)
});
Then, you can still call your functions the same way you normally would:
store.model.add(5); // store.model.count === 5
store.model.up(); // store.model.count === 6
store.model.down(); // store.model.count === 5
If you're using TypeScript, be aware that this will mess with the type definitions, because we're changing the function signature!
store.model
An object composed of all values and proxied functions passed to createStore
.
To call suscriptions when proxied, model functions should return (or resolve to) an object.
store.set
Merges some data into the store model at the root level and calls store.update
.
It's a built-in shortcut for this:
const store = createStore({
set(data) {
return data;
}
});
In that case, store.set
will do the same thing as store.model.set
.
store.subscribe
Calls the passed function every time a model function that returns (or resolves to) an object is executed.
Returns an unsubscribe
function that you can call to remove the subscription.
Tread lightly when rendering in subscriptions - they're not throttled or rate-limited in any way!
store.update
Calls all subscriptions manually. You usually only do this once after creating the store.
A wise man once said:
Return the change that you wish to see in the world.
When you call a function that returns (or resolves to) an object, the data is deeply merged into the current model:
const store =
createStore({ a: 1, b: { c: 2, d: 3 }, set: (data) => data });
store.model.set({ b: { d: 4 }, setA: (value) => ({ a: value }) });
// New model: ({ a: 1, b: { c: 2, d: 4 }, set: (data) => data, setA: (value) => ({ a: value }) });
In this case, set
allows changing any property of the model, while setA
only allows changing the a
property.
Functions are proxied to automatically call store.update
if they return (or resolve to) an object when executed.
So if you call store.model.setA(5)
, it will call store.update
afterwards as well.
Promises are supported out of the box - store.update
is called after the promise resolves:
export const CounterModel = {
count: 0,
async down() { // sweet async / await goodness 🍰
const value = await Promise.resolve(-1); // Get the value from some remote server
return { count: this.count + value });
},
up() { // ye olde promises 🧓
return Promise.resolve(1).then((value) => ({ count: this.count + value });
}
};
You can put objects inside objects:
export const ABCounterModel = {
counterA: CounterModel,
counterB: CounterModel
};
This allows you to build a hierarchical tree of data and functions.
So you want to do code splitting with webpack and have functions from the imported modules automatically call store.update
data when executed?
Here are a few ways to do it:
const store = createStore();
// Get a named export
import('./counter-model').then((exports) => store.set({ counter: exports.CounterModel }));
// Get multiple named exports
import('./another-model').then((exports) => store.set({ A: exports.ModelA, B: exports.ModelB }));
// Get default export
import('./yet-another-model').then((exports) => store.set({ C: exports.default }));
// Get all exports
import('./utils-model').then((exports) => store.set({ utils: exports }));
When the import
promise resolves, the model's functions proxied from CounterModel
will automatically call store.update
when executed.
You shouldn't have to (and can't always) rely on the store being available. Encapsulating your models makes them decoupled from 3rd party libraries, which means they're easier to maintain and adaptable to flexible requirements.
So to lazy load data without touching the store, you can do this:
export const LazyLoadedModel = {
set(data) {
return data;
},
loadChildModels() {
// Get a named export
import('./counter-model').then((exports) => this.set({ counter: exports.CounterModel }));
// Get multiple named exports
import('./another-model').then((exports) => this.set({ A: exports.ModelA, B: exports.ModelB }));
// Get default export
import('./yet-another-model').then((exports) => this.set({ C: exports.default }));
// Get all exports
import('./utils-model').then((exports) => this.set({ utils: exports }));
}
};
Then define your store and load the models:
const store = createStore({ lazy: LazyLoadedModel });
store.model.lazy.loadChildModels();
The child models will be inserted into the model's data when the import is done.
Derpy is written in TypeScript, so if you use it you get autocomplete and type checking out of the box.
Going back to the Counter example:
store.model.up(5); // [ts] Expected 0 arguments, but got 1.
However, this
doesn't get type definitions inside objects:
export const CounterModel = {
count: 0,
add(value: number) {
return { count: this.count + value }; // Hmm, `this` is of type `any` here 😕
}
};
And we can't do add(this: typeof CounterModel, value: number)
either, because we're referencing an object inside its own definition.
So...read on.
To get type safety inside your models, or if you just prefer to, you can use classes instead of objects:
export class CounterModel {
count = 0;
add(value: number) { // or `add(value) {` if you don't use TypeScript
return { count: this.count + value }; // Yay, `this` is of type `CounterModel` 😄
}
};
And then when creating your store:
const store = createStore(new CounterModel());
store.model.add('1'); // [ts] Argument of type '"1"' is not assignable to parameter of type 'number'.
Be careful with those if you're using this
inside your model functions - as expected, it would refer to the parent context. Because functions are proxied when the store is created, class methods defined as arrow functions won't refer to the correct this
either.
First, make sure you have the Redux devtools extension for your browser. Then:
import { createStore } from 'derpy';
import { debug } from 'derpy/debug/redux-devtools';
import { CounterModel } from './counter-model';
const store = process.env.NODE_ENV === 'production' ? createStore(CounterModel) : debug(createStore(CounterModel));
For examples with different view layers, see the CodePen collection.
Here's a counter example with picodom:
/** @jsx h */
import { createStore } from 'derpy';
import { app } from 'derpy/app/picodom';
import { h, patch } from 'picodom';
import { CounterModel } from './counter-model';
const Counter = ({ model }) =>
<div>
Your count is: {model.count}
<button onclick={model.down}>-</button>
<button onclick={model.up}>+</button>
</div>
;
app({
store: createStore(CounterModel),
view: Counter,
patch
});
All functions in the model are bound to the correct context, so you can write onclick={model.up}
instead of onclick={() => model.up()}
.
The app
function is a very thin layer on top of Derpy to reduce boilerplate.
You can pass a custom DOM element to render into as the second argument, which is document.body
by default.
It also returns an unsubscribe
function to stop rendering, effectively "destroying" your app, although the store will still work just fine.
app
uses requestAnimationFrame
by default to throttle rendering. Alternatively, provide your own function in app({ throttle: ... })
.
this
is bad and you should feel bad 🦀Hey, that's not a question! Anyway, if you prefer state
or something else instead of this
, you can use the callFunction
option when creating a store, as described in the custom function calls section.
I'm glad you asked! Here are some useful resources:
FAQs
A silly little state manager 😋
The npm package derpy receives a total of 3 weekly downloads. As such, derpy popularity was classified as not popular.
We found that derpy demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.