I'm still wrapping my head around RxJS and there is this pattern I keep running into and that I would like to find a more elegant way to write.
Implementing the model part of a Model-View-Intent pattern component, I have a function that takes actions as input an returns a single state$ Observable as output.
function model(actions) {
const firstProperty$ =
const anotherProperty$ = …
// Better way to write this?
const state$ = Rx.Observable.combineLatest(
firstProperty$, anotherProperty$,
(firstProperty, anotherProperty) => ({
firstProperty, anotherProperty
})
);
return state$;
}
So my model method computes a bunch of observables, every one of them emit items that represents a part of the state of my application. That is fine.
But how to I cleanly combine them into a single one observable that emits states, each state being a single object whose keys are the initial observable names?
I borrowed this pattern from https://github.com/cyclejs/todomvc-cycle :
function model(initialState$, actions){
const mod$ = modifications(actions)
return initialState$
.concat(mod$)
.scan( (state, mod) => mod(state))
.share()
}
function modifications(actions){
const firstMod$ = actions.anAction$.map(anAction => (
state => ({ ...state,
firstProperty: anAction.something
})
const secondMod$ = actions.otherAction$.map(otherAction => (
state => ({ ...state,
firstProperty: otherAction.something,
secondProperty: aComputation(otherAction)
})
return Rx.Observable.merge([firstMod$, secondMod$ ]).share()
}
In the main function :
const initialState$ = Rx.Observable.from({})
const actions = intent(DOM)
const state$ = model(initialState$, actions).share()
Using help from CHadrien, here is a working solution.
const prop1$ = Rx.Observable.of('foo');
const prop2$ = Rx.Observable.of('bar');
const prop3$ = Rx.Observable.of('baz');
const prop4$ = Rx.Observable.of('foobar');
function combineObservables(objectOfObservables) {
const keys = Object.keys(objectOfObservables);
const observables = keys.map(key => objectOfObservables[key]);
const combined$ = Rx.Observable.combineLatest(
observables, (...values) => {
var obj = {};
for (let i = 0 ; i < keys.length ; i++) {
obj[keys[i]] = values[i];
}
return obj;
}
);
return combined$;
}
combineObservables({prop1$, prop2$, prop3$, prop4$}).subscribe(x => console.log(x));
And the result:
[object Object] {
prop1$: "foo",
prop2$: "bar",
prop3$: "baz",
prop4$: "foobar"
}
Related
React suggests not to mutate state. I have an array of objects which I am manipulating based on some events. My question is, is it okay to write it like this:
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
function SomeComponenet() {
const [filters, setFilters] = useState(aemFilterData);
const handleFilterClick = (filter, c) => {
let copiedFilters = makeCopy(filters);
/**
* Apply toggle on the parent as well
*/
if (!("parentId" in filter)) {
copiedFilters[filter.id].open = !copiedFilters[filter.id].open;
}
setFilters(copiedFilters);
}
}
Am I mutating the original object by doing like above? Or does it make a difference if written like this:
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
function SomeComponent() {
const [filters, setFilters] = useState(aemFilterData);
const handleFilterClick = (filter, c) => {
let copiedFilters = makeCopy(filters);
/**
* Apply toggle on the parent as well
*/
if (!("parentId" in filter)) {
copiedFilters = copiedFilters.map((f) => {
if (filter.id === f.id) {
return {
...f,
open: !f.open,
};
} else {
return { ...f };
}
});
}
setFilters(copiedFilters);
}
}
What's the preferred way to do this? Spread operators are getting a lot verbose and I am not liking it, but I prefer it if that's how I need to do it here. immutable.js and immer or not an option right now.
const makeCopy = (arr) => arr.map((item) => item );
With above code, it's mutating on the original object reference because we're not creating a deep clone.
copiedFilters[filter.id].open = !copiedFilters[filter.id].open;
Here reference of copiedFilters[filter.id] and filters[filter.id] is same.
With spread operator
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
Here we create a new copy of the inner object too. So copiedFilters[filter.id] and filters[filter.id] will have different reference.
This is same as your second approach.
So either you use spread operator while making a copy or you can skip making a copy in the second approach and directly map on filters since you're using spread operator there. This looks better because, why run loop twice - first to create copy and then to update open.
// let copiedFilters = makeCopy(filters); Not needed in second approach
copiedFilters = copiedFilters.map((f) => {
if (filter.id === f.id) {
return {
...f,
open: !f.open,
};
} else {
return { ...f };
}
});
You can create a deep clone when you copy but that would be waste of computation and memory, I don't think it's needed here.
Deep clone is helpful when you have further nesting in the object.
I am learning functional programming with javascript. I have learned that 2 parameters are needed for reduce. Accumalator and the actual value and if we don't supply the initial value, the first argument is used. but I can't understand how the purchaseItem functions is working in the code below. can anyone please explain.
const user = {
name: 'Lachi',
active: true,
cart: [],
purchases: []
}
let history = []
const compose = (f, g) => (...args) => f(g(...args))
console.log(purchaseItem(
emptyCart,
buyItem,
applyTaxToItems,
addItemToCart
)(user, {name: 'laptop', price: 200}))
function purchaseItem(...fns) {
console.log(fns)
return fns.reduce(compose)
}
function addItemToCart (user, item) {
history.push(user)
const updatedCart = user.cart.concat(item)
return Object.assign({}, user, { cart: updatedCart })
}
function applyTaxToItems(user) {
history.push(user)
const {cart} = user
const taxRate = 1.3
const updatedCart = cart.map(item => {
return {
name: item.name,
price: item.price * taxRate
}
})
return Object.assign({}, user, { cart: updatedCart })
}
function buyItem(user) {
history.push(user)
return Object.assign({}, user, { purchases: user.cart })
}
function emptyCart(user) {
history.push(user)
return Object.assign({}, user, {cart: []})
}
Maybe it helps if you take a minimal working example and visualize the output structure:
const comp = (f, g) => x => f(g(x));
const inc = x => `inc(${x})`;
const sqr = x => `sqr(${x})`;
const id = x => `id(${x})`;
const main = [sqr, inc, inc, inc].reduce(comp, id);
console.log(main(0)); // id(sqr(inc(inc(inc(0)))))
Please note that we need id to allow redicung an empty array.
It's a way of creating a pipeline of functions whereby the output from one function is used as the parameter of the next, so we end up with a composed function that is effectively
(...args) =>
emptyCart(
buyItem(
applyTaxToItems(
addItemToCart(...args)
)
)
)
Writing the reduce out in longhand might help in understanding:
fns.reduce((acc, currentFn) => compose(acc, currentFn))
I have multiple mobx stores and find myself having actions in each of them that are pretty much identical. I'm therefor hoping to be able to generalise and reuse them between stores. Below I have tried to break out the create action hoping to be able to import it to multiple stores but it doesn't work as self is not available.
I want to go from this:
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
const create = flow(function* create(newItem) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
self[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
return {
create
}
})
To something like this, where the create action could be reused in other stores:
const create = flow(function* create(newItem, collection) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
this[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
const _create = create.bind(self)
return {
_create
}
})
Any ideas for how to achieve this?
While I've never done anything like that, but I was thinking to and had an impression that it should work. But if it doesn't, you can do something like:
const create = (self) => flow(function* create(newItem, collection) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
self[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
return {
create: create(self)
}
})
This should definitely work.
I wanna set multiple object property with the same value.
const SHOW_PAYMENT_DIALOG = 'SHOW_PAYMENT_DIALOG';
const SHOW_BUSINESS_DIALOG = 'SHOW_BUSINESS_DIALOG';
const handler = (state, payload) => {
return {
...state,
data: payload
};
};
const object = {
[SHOW_BUSINESS_DIALOG]: handler,
[SHOW_PAYMENT_DIALOG]: handler,
};
As above example, I have to manually assign handler for 2 property SHOW_BUSINESS_DIALOG & SHOW_PAYMENT_DIALOG
Is there anyway to set it quickly on the fly by js api but no need to introduce a new function to handle that like
const object = {
[SHOW_BUSINESS_DIALOG, SHOW_PAYMENT_DIALOG]: handler,
};
You could use Object.fromEntries() with .map() if you're okay with having the same reference to handler for each value... (the snippet console output shows how the handler method is the same reference for each value, that's why it looks a little strange):
const SHOW_PAYMENT_DIALOG = 'SHOW_PAYMENT_DIALOG';
const SHOW_BUSINESS_DIALOG = 'SHOW_BUSINESS_DIALOG';
const handler = (state, payload) => {
return {
...state,
data: payload
};
};
const keys = [SHOW_PAYMENT_DIALOG, SHOW_BUSINESS_DIALOG];
const object = Object.fromEntries(keys.map(k => [k, handler]));
console.log(object);
Please note that .fromEntries() is currently in draft mode, however, I think a generic if statement accompanied with a Set (using .has()) would be better for this case than using an object:
const SHOW_PAYMENT_DIALOG = 'SHOW_PAYMENT_DIALOG';
const SHOW_BUSINESS_DIALOG = 'SHOW_BUSINESS_DIALOG';
const handler = (state, payload) => {
return {
...state,
data: payload
};
};
const get_handler = key => {
const keys = new Set([SHOW_PAYMENT_DIALOG, SHOW_BUSINESS_DIALOG]);
if(keys.has(key))
return handler;
}
console.log(get_handler(SHOW_BUSINESS_DIALOG)); // hanlder
console.log(get_handler("foo")); // undefined
You can create a generic function and pass an array of key and loop through keyArr and place value for each key
const SHOW_PAYMENT_DIALOG = 'SHOW_PAYMENT_DIALOG';
const SHOW_BUSINESS_DIALOG = 'SHOW_BUSINESS_DIALOG';
let value = "some value"
const object = { someKey: 'value'};
let dynamicSetValues = (keyArr, value) => {
keyArr.forEach(key => {
object[key] = value
})
}
dynamicSetValues(['SHOW_PAYMENT_DIALOG','SHOW_BUSINESS_DIALOG'], value)
console.log(object)
Note:- this mutates original object, if you want immutability you can make a copy of object and place value on desired keys and return a new object every time from function
Hy, i faced a problem with RxJS Combination operators...
here is example object:
const userData = {
dbKeyPath: 'www.example.com/getDbKey',
users:[
{name:'name1'},
{name:'name2'},
{name:'name3'}
]
}
Make observable from them:
const userDataStream = Rx.Observable.of(userData)
const dbKeyStream : string = this.userDataStream.mergeMap(_userData => getDbKey(_userData.dbKeyPath))
const userStream = this.userDataStream.pluck('users').mergeMap(_users=>Rx.Observable.from(_users))
My expected result is stream with combined observables:
[user[0],dbKey],[user[1],dbKey],[user[2],dbKey]...
It works pretty well with withLatestFrom operator:
const result = userStream.withLatestFrom(dbKeyStream) // [user, dbkey]
But, how can i archive same result when i apply .delay() operator to dbKeyStream ?
I would suggest using the mergeMap overload with the selectorFunc:
const userData = {
dbKeyPath: 'www.example.com/getDbKey',
users:[
{name:'name1'},
{name:'name2'},
{name:'name3'}
]
};
function getDbKey(path) {
return Rx.Observable.of('the-db-key:'+path)
.do(() => console.log('fetching db key for path: '+ path))
.delay(1000);
}
const userDataStream = Rx.Observable.of(userData)
.mergeMap(
_userData => getDbKey(_userData.dbKeyPath),
(_userData, dbKey) => _userData.users.map(_usr => ({ user: _usr, dbKey }))
)
.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
This gives you the input object and each output value to combine together as you require.