effector
effector copied to clipboard
useStoreMap: Simpler method to get a partial object/array
I'd like to just get certain properties on an object. This seems very verbose to accomplish the task:
const pick = <T, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> => keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
const partialState = useStoreMap({
fn: pick,
keys: ['firstKey', 'secondKey'] as const,
store: exampleObjectStore, // Record<string, T>
});
Is there a simpler, type-safe way to get a partial object? Something like this would be ideal:
const partialState = useStoreMap({
keys: ['firstKey', 'secondKey'],
store: exampleObjectStore, // Record<string, T>
});
It would work with arrays in the same way:
const partialState = useStoreMap({
keys: [1, 3],
store: exampleArrayStore, // T[]
});
You can read your fields directly with useStore
const $user = createStore({name: 'alice', age: 22})
export const User = () => {
const {name, age} = useStore($user)
...
}
Objects in your store could be already prepared for your use case, that’s the case of store.map and combine
@zerobias Thank you for responding.
You can read your fields directly with useStore
const $user = createStore({name: 'alice', age: 22}) export const User = () => { const {name, age} = useStore($user) ... }
In this example, you are using every property in the state (name and age).
Instead, if:
- you wrote
const {name} = useStore($user), and - the store was updated with a different value for
agebut the same value forname
would the component re-render or not?
Objects in your store could be already prepared for your use case, that’s the case of store.map and combine
These methods still require writing a function, which is what this feature request is about trying to avoid. It would be nice to have a simple way to get a slice of an object by just passing the store and property accessors (keys) only, without any function.
Store is updated when it receives a value that is not equal (!==) to current one and to undefined, so component will rerender even with your pick function.
But you might prepare your data with store.map: it has a second argument with current store value:
const $user = createStore({name: 'alice', age: 22, email: '[email protected]'})
const $userTitle = updateBy($user, ['name', 'email'])
export const UserTitle = () => {
const {name, email} = useStore($userTitle)
...
}
function updateBy(sourceStore, fields) {
return sourceStore.map((update, currentValue) => {
if (!currentValue) return update
for (const field of fields) {
if (update[field] !== currentValue[field])
return update
}
})
}
I'm found that documentation doesn't mention store update conditions, so I added it to Store article
New features are added to the library when there are enough feature requests from users, as almost every project is unique with its own unique demands, including updates by only part of the store.
Also, usually complex stores are composed from a simple ones with combine, so if you split your store and combine only required fields, then it will trigger rerender only when needed:
const $name = createStore('alice')
const $age = createStore(21)
const $email = createStore('[email protected]')
const $userTitle = combine({
age: $age,
email: $email
})
@zerobias Thanks for providing those examples.
In this case, I am in need of the opposite of combine: I need to only receive updates when selected keys of the store object change. How can I accomplish that?
Split monolithic single store with irrelevant fields to atomic stores and combine only necessary ones. If your use cases assumes that you need more than one combination then just make another combine call. That's why atomic stores exists in a first place
Thanks, again.
Split monolithic single store with irrelevant fields to atomic stores and combine only necessary ones. If your use cases assumes that you need more than one combination then just make another combine call. That's why atomic stores exists in a first place
🤔 I was hoping this is not the only way.
I am receiving this "monolithic" store from a module that I don't control. It would be very nice to be able to "pick" or "select" property values from an object store in cases like this.
Initially, I thought that's what useStoreMap might let me do, but now I realize that it's not currently intended for that purpose.
Considering this concept, will you please regard this issue as a feature request for that purpose? If you are open to this feature request, then I have more thoughts to share about the functionality and API for it.
Here's an example of what describing (the partialStore function), but it only works for for objects (not arrays), and only top level keys (not nested ones):
const pick = <T, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> => keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
const partialStore = <T, K extends keyof T>(
store: Store<T>,
keys: readonly K[],
): Store<Pick<T, K>> => store.map((state, lastState?: Pick<T, K>) => {
if (typeof lastState === 'undefined') return pick(state, keys);
for (const key of keys) {
if (state[key] !== lastState[key]) return pick(state, keys);
}
return lastState;
});
example.deno.ts:
// @deno-types='https://unpkg.com/[email protected]/effector.mjs.d.ts'
import {
combine,
createEvent,
createStore,
Store,
} from 'https://unpkg.com/[email protected]/effector.mjs';
const pick = <T, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> => keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
const partialStore = <T, K extends keyof T>(
store: Store<T>,
keys: readonly K[],
): Store<Pick<T, K>> => store.map((state, lastState?: Pick<T, K>) => {
if (typeof lastState === 'undefined') return pick(state, keys);
for (const key of keys) {
if (state[key] !== lastState[key]) return pick(state, keys);
}
return lastState;
});
const replaceWithValue = <T>(state: T, value: T): T => value;
const defaultState = {
item: 'bagel',
qty: 2,
type: 'plain',
};
const setItem = createEvent<string>('setItem');
const setQty = createEvent<number>('setQty');
const setType = createEvent<string>('setType');
const $item = createStore(defaultState.item).on(setItem, replaceWithValue);
const $qty = createStore(defaultState.qty).on(setQty, replaceWithValue);
const $type = createStore(defaultState.type).on(setType, replaceWithValue);
const $obj = combine({item: $item, qty: $qty, type: $type});
const $partial = partialStore($obj, ['qty', 'type']);
$obj.watch(state => console.log('$obj', state));
// $obj { item: "bagel", qty: 2, type: "plain" }
$partial.watch(state => console.log('$partial', state));
// $partial { qty: 2, type: "plain" }
setQty(1);
// $obj { item: "bagel", qty: 1, type: "plain" }
// $partial { qty: 1, type: "plain" }
setItem('criossant');
// $obj { item: "criossant", qty: 1, type: "plain" }
setType('cinnamon');
// $obj { item: "criossant", qty: 1, type: "cinnamon" }
// $partial { qty: 1, type: "cinnamon" }
const $partial = combine({qty: $qty, type: $type})
You just don’t need any intermediate $obj store for that task.
Even more, combine support arrays too:
const $qtyTypeTuple = combine([$qty, $type])
Of course, it is just for example. But like I said before:
I am receiving this "monolithic" store from a module that I don't control. It would be very nice to be able to "pick" or "select" property values from an object store in cases like this.
In the example, $obj is the store I am receiving from the module, and it is the only store the module exports.
You can review patronum/spread method for your case.
const $first = createStore("")
const $second = createStore("")
const receiveMonolith = createEvent()
spread({
source: receiveMonolith,
targets: {
first: $first,
second: $second,
},
})
receiveMonolith({
first: "Hello",
second: "World",
id: 123,
})