We have a case where we need to pass a function callback is required on one of our models. something like this:
const Column = t.model({
key: t.string,
customRenderer: t.function
})
any ideas on how to handle this use case?
You cannot store functions in model properties, as they are non-serializable. However, you can use volatile state to store a function (or: provide them through the environment if the function never changes)
const Column = t.model({
key: t.string,
customRenderer: t.function
})
.actions(self => {
let customRenderer = null
return {
setCustomRenderer(f) { customRenderer = f },
getCustomRenderer() { return customRenderer }
})
have a similar case here.
We want to create a form engine, which can create a form via some conf.
items: [
{
type: 'input',
title: 'Get Coffee',
},
{
type: 'raido',
title: 'Write simpler code',
},
],
So, input would create an input, radio type would create a radio.
But we still want to have the form item with the ability of being customized.
{
type: 'custom',
title: 'custom one',
componentName: 'excalibur',
}
So function needed to be passed down to the form engine.
.actions(self => {
let componentList = {}
return {
registerComponent(item) {
componentList[item.componentName] = item.element
},
getRegisteredComponent(componentName) {
if (componentName) {
return componentList[componentName]
}
},
add(item) {
self.items.push(item)
}
}
})
I've created a demo here.
Question are
Functions cannot be stored as props in the tree because they are not serializable. However, since version 1.2 you can declare volatile properties with .volatile, which makes the above pattern a lot more simple straight forward!
const Column = t.model({
key: t.string,
})
.volatile(self => ({
customRenderer: t.function
}))
.actions(self => ({
setCustomRenderer(f) {
self.customRenderer = f
}
})
Volatile is great, but types.function does not exist ... what to use?
@tomitrescak I use this:
const parseFunction = (value: string) => {
const fn = eval(`(${value})`);
if (typeof fn !== 'function') throw new Error(`${value} is not a valid function`);
return fn;
};
export const functionType = types.custom<string, Function>({
name: 'functionType',
fromSnapshot(value: string) {
return parseFunction(value);
},
toSnapshot(value: Function) {
return value.toString();
},
getValidationMessage(value: string) {
try {
parseFunction(value);
return '';
} catch (e) {
return `value "${value}" is Not a valid function ${e}`;
}
},
isTargetType(value: any) {
return value instanceof Function;
}
});
It works for my needs, but of course this method won't work universally. The goal is to have a serializable (and deserializable) representation of instances of the type (functions in this case).
Fair enough, thanks!!
In my case I have an array of operations that are only executed when online. So they are written into types.array(functionType) (https://github.com/mobxjs/mobx-state-tree/issues/491#issuecomment-415864489).
As they are now serializable, they are written to indexed-db (thus survive reloading) and executed the next time the app is online.
Thanks a lot @srolel
As my operations were queries and they did not serialize as nicely as a simple console.log call, I ended up building a new Type:
import { types } from 'mobx-state-tree'
export default types.model('Query', {
name: types.string,
variables: types.maybeNull(types.string, null),
})
This is used in the model:
pendingQueries: types.optional(types.array(QueryType), [])
and a reaction:
reaction(
() => `${self.pendingQueries.length}/${self.online}`,
() => {
if (self.online) {
const query = self.pendingQueries[0]
if (query) {
const { name, variables } = query
try {
variables ? self[name](JSON.parse(variables)) : self[name]()
} catch (error) {
return
}
}
// remove operation from queue
self.pendingQueries.shift()
}
},
)
@srolel
I am using the code you commented very well.
However, I am writing a comment because I thought it would be good to share only one part of your code about eval().
const parseFunction = (value) => {
try {
const fn = Function(`'use strict'; return (${value});`)();
return fn;
} catch(e) {
throw new Error(`${value} is not a valid function`);
}
};
Most helpful comment
@tomitrescak I use this:
It works for my needs, but of course this method won't work universally. The goal is to have a serializable (and deserializable) representation of instances of the type (functions in this case).